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/CHANGELOG.md CHANGED
@@ -1,5 +1,617 @@
1
1
  # Changelog
2
2
 
3
+ All notable changes to this project are documented here.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Changed — the signup clickwrap is one line
10
+
11
+ - **`form.clickwrap` renders ONE checkbox carrying ONE sentence** whenever every
12
+ statement in a policy is an ordinary, required, default-worded `agree_to` or
13
+ `acknowledge`:
14
+
15
+ ☐ I agree to the Terms of Service and I acknowledge the Privacy Policy.
16
+
17
+ The documents are linked inside the sentence, and the label IS the sentence —
18
+ pressing the words toggles the control and assistive technology announces both
19
+ together. Gone from the rendering: the "Required" flag, the visible "(opens in
20
+ a new tab)" hint, the version label under each link, and the list of documents
21
+ under each statement. The `required` attribute stays as progressive
22
+ enhancement (THE SERVER DECIDES is unchanged), the new-tab truth is now an
23
+ `sr-only` span rendered only when the link really does open one, and versions
24
+ stay on receipts, where somebody reading the record can act on them.
25
+
26
+ The evidence is unchanged. One control, several statements: the manifest signs
27
+ which statement keys it covers and which key it is submitted under, and the
28
+ server fans that one answer out to all of them — each recorded with its own
29
+ kind, documents, assertion, and lifecycle, exactly as before. An unticked or
30
+ absent control refuses every one of them. Whether one answer may cover several
31
+ statements is re-checked at capture against the FROZEN POLICY REVISION, not
32
+ taken on the token's word.
33
+
34
+ Nothing that a person could reasonably want to answer differently is folded
35
+ in: optional consents, recorded yes/nos, statements with a withdrawal route,
36
+ and copy an application wrote itself all keep a control of their own below the
37
+ line. A policy with nothing composable — operator attestation rails, payout
38
+ authorizations — renders exactly as it did before. `combined: false` on
39
+ `form.clickwrap` / `form.clickwrap_fields` asks for the itemized shape, and it
40
+ reaches the presenter rather than the template, so the manifest signs the
41
+ shape that was offered. An itemized manifest is byte-identical to the ones
42
+ earlier versions wrote.
43
+
44
+ The words are translations: `clickwrap.sentence.agreement`,
45
+ `clickwrap.sentence.acknowledgment`, and three connectives, with `%{documents}`
46
+ marking where the links go. A locale that has not translated them renders the
47
+ itemized shape rather than half an English sentence.
48
+
49
+ Custom surfaces iterate `presentation.itemized_statements` rather than
50
+ `statements`, and render `clickwrap_combined_sentence(presentation.combined)`
51
+ when there is one. The presentation linter keeps its preselected-control and
52
+ action-ordering rules and gains
53
+ `combined_statement_rendered_as_its_own_control`, which is where an ejected
54
+ view lands after this change.
55
+
56
+ - **Receipts record the sentence that was read.** `presentation` gained
57
+ `combined_sentence` and `combined_statements`, shown on the HTML receipt and
58
+ the engine's receipt screen. The acts say what was recorded; this says what
59
+ was on the screen. Both are absent — not null — on an itemized presentation,
60
+ so every receipt written before this change verifies byte for byte exactly as
61
+ it did.
62
+
63
+ ### Added — `link:`, the page a person actually reads a document on
64
+
65
+ - **`Clickwrap.document :terms, from: ..., link: "/legal/terms"`** presents and
66
+ signs the host's own formatted page instead of the engine's rendering of the
67
+ published bytes. It is the path rendered AND the path signed, so evidence
68
+ never cites a different target from the link somebody pressed, and the trade
69
+ is written where a reviewer sees it: a host page shows whatever is current, so
70
+ the signed path is a stable address rather than an immutable snapshot. The
71
+ bytes stay frozen, digested, and recorded either way.
72
+
73
+ Precedence: a resolver passed at the call site, then `link:`, then the render
74
+ context's default resolver (the mounted engine route, with this request's
75
+ Hotwire Native treatment attached — which therefore applies to `link:` paths
76
+ unchanged), then the engine's own routes. The refusal to sign a link an
77
+ unmounted engine cannot resolve is about the ENGINE route, so a policy whose
78
+ every document names a host page now presents with no mount at all, and the
79
+ refusal keeps firing for every document that does not.
80
+
81
+ A `link:` is checked at boot for a scheme a browser can navigate:
82
+ `javascript:`, `data:`, a bare word, and protocol-relative `//host` are
83
+ refused with the sentence that fixes them.
84
+
85
+ - The presenter's framework wiring moved from `document_version_path_with:` to
86
+ `default_document_version_path_with:`, leaving the former as what it always
87
+ read like — the host's own resolver, which still wins outright.
88
+ `clickwrap_document_version_path_for_presentation` gained a `declared_link:`
89
+ keyword.
90
+
91
+ ### Added — the quickstart runs in CI
92
+
93
+ - **The README quickstart is now executed end to end**, in a subprocess, against
94
+ a throwaway application built only from what the installer emitted: install →
95
+ migrate → declare → `has_clickwraps` → publish → render the form → submit the
96
+ token that render produced → the evidence row exists → the receipt verifies.
97
+ It is the one document every user reads and was the only one nothing checked.
98
+
99
+ It found real defects on its first run, all of them consequences of emitting
100
+ fewer tables: capture marked a presentation accepted without asking whether
101
+ the policy retains presentations, and receipts, verification, and the
102
+ retention planner read optional annex tables unconditionally. Every
103
+ association whose table is optional now answers "there is nothing here" when
104
+ the table was never created — which is not a fallback but the exact truth,
105
+ since without the table no row was ever written.
106
+
107
+ ### Changed — the emitted footprint
108
+
109
+ - **The generated initializer states decisions, not defaults.** It carried about
110
+ thirty live lines that assigned the gem's own default back to itself, so a
111
+ reader could not tell which lines somebody had chosen and which were noise —
112
+ and every one of them was a line to maintain in two places forever. Live lines
113
+ are now only what the installer actually decided or detected; every other
114
+ setting appears commented, with its default value, under prose that says what
115
+ it does. A default install drops from about thirty live settings to three,
116
+ plus the eleven request-evidence lines.
117
+
118
+ Those eleven stay live even when every answer is `false`, deliberately: each
119
+ is an answer to a question the installer asked, and "we decided not to collect
120
+ this" is a decision worth reading rather than inferring from a file that does
121
+ not mention it. The file says so where the rule is stated.
122
+ - **`clickwrap:install` emits only the tables an installation can put a row
123
+ in.** Seven of the seventeen tables — persisted presentations, the
124
+ request-evidence annex, chain heads, integrity attestations, external
125
+ actions, disposition plans, legal holds — are each gated on a configuration
126
+ that is off by default, so a default install could never write to any of
127
+ them. A schema that contains them anyway claims capabilities and data
128
+ categories the application does not have, which is exactly the impression
129
+ this gem exists not to give. Each now arrives with its own flag
130
+ (`--with-persisted-presentations`, `--with-request-evidence`,
131
+ `--with-integrity`, `--with-retention-ops`, `--with-external-actions`),
132
+ following the `clickwrap:hardening` precedent, and re-running the generator
133
+ later with a flag adds that migration then.
134
+
135
+ Enabling a request-evidence field implies `--with-request-evidence`: an
136
+ installation that records IP addresses into a table it never created is not
137
+ a schema choice, and the operator already answered the question that matters.
138
+
139
+ The one failure mode this trade creates — turning a capability on and
140
+ forgetting its migration — is caught three ways, each naming the exact
141
+ command: at boot, by `bin/rails clickwrap:doctor`, and at the entry points
142
+ no configuration announces (`authorize_external_action!`, legal holds,
143
+ disposition planning). The boot check deliberately stays quiet while
144
+ migrations are pending, because refusing to boot would make the fix
145
+ unrunnable.
146
+
147
+ ### Fixed — the two-audit adversarial review
148
+
149
+ Two independent audits read the gem as an unfamiliar developer would: one
150
+ followed the installer and the README literally, the other read the code
151
+ against its own claims. Everything below is a defect they evidenced.
152
+
153
+ - **The installer and the README never taught the auth door.** A Devise app
154
+ that followed the post-install message verbatim created accounts with no
155
+ evidence at all, silently — the gem is never called on that path, so nothing
156
+ could warn about it. The post-install message and the README quickstart now
157
+ both carry the door step, with the exact line, the host's own file path and
158
+ class name, and the `devise_for ..., controllers:` route that makes the
159
+ subclass reachable. "The installer detects your authentication stack and
160
+ generates an explicit adapter" now says what it does: it detects, and prints
161
+ the line for you to add.
162
+ - **`renew!`, `correct_declaration!`, and `change_consent_scope!` had no tests,
163
+ no documentation, and no callers.** They are the conceptual spine of the six
164
+ verbs, so they are now tested behaviorally — the event is appended, the
165
+ earlier event still says exactly what it said and still verifies, and the new
166
+ receipt verifies on its own — and the README shows each one with the sentence
167
+ that says what it means. Writing those tests surfaced the reason nobody had
168
+ used them: all three capture through a real presentation, so each needs a
169
+ `submission:` exactly as the original statement did. That is correct — a
170
+ correction, a renewal and a rescope are new statements by the same person,
171
+ not administrative flags — but it was nowhere in the docs, and the README now
172
+ says it.
173
+ - **`config.actor_class_name` now decides something.** The installer spends
174
+ sixty lines, a `--actor-class` option, and a seven-line warning on this
175
+ setting because "a wrong guess attributes evidence to the wrong kind of
176
+ record for years" — which was not true, because nothing checked. The setting
177
+ fed one error string, and `Configuration#actor_class` had no production
178
+ caller at all. Capture now asserts the recorded actor is an instance of it,
179
+ with an error that names the setting and points an organization at
180
+ `acting_for:` where it belongs. System actors, anonymous actors, and literal
181
+ references to actors in other systems are their own kinds, say so in the
182
+ receipt, and are not checked against the class.
183
+ - **The receipt list authorized after paging.** Taking a page of rows and
184
+ filtering them afterwards rendered "you have no receipts" whenever the
185
+ viewer's newest page happened to be rows the host would not show them, while
186
+ readable ones sat past the limit. Authorization now happens first, in bounded
187
+ batches, and the actor is eager-loaded — a full page is three queries instead
188
+ of fifty-two.
189
+ - **A presentation resolved its documents one statement at a time**, including
190
+ the same document twice when two statements named it, even though documents
191
+ are immutable and published and the answer cannot differ. Two queries now
192
+ resolve every document a policy references.
193
+ - **`Clickwrap.reset!` left the presentation-manifest verifier memoized**, so a
194
+ verifier built under the previous configuration survived the reset. The test
195
+ suite had been resetting it by hand, which is why nothing caught it.
196
+ - **`Clickwrap.verify(event_id, …)` silently dropped its strictness keywords.**
197
+ The event-id branch accepted `require_current_revision:` and then never
198
+ received it, so a host asking "is this old evidence still good?" about one
199
+ recorded act got an unqualified yes — worse than an error, because it looked
200
+ like an answer. Both branches now answer the whole question: the event id
201
+ compares the act's recorded policy revision against the wording compiled
202
+ today, and re-derives the subject fingerprint from the live record when
203
+ `subject:` is passed. A policy that is no longer declared answers
204
+ `:unknown_policy` rather than passing, because "we can no longer check this"
205
+ must never be spelled the same way as "this is fine". `Clickwrap.verify(event_id,
206
+ subject:, require_current_revision: true)` is therefore the complete public
207
+ form of that question, and nothing needs to reach into
208
+ `Clickwrap::PolicyRevision` or `Clickwrap::SubjectFingerprint` to ask it.
209
+ Verifying an event id also no longer writes: the revision comparison is by
210
+ digest, so a read-only question stops creating a revision row as a side
211
+ effect.
212
+ - **An unmounted engine used to sign 404 document links into evidence.** The
213
+ document link goes into the presentation manifest, into the digest, and into
214
+ the record of what was offered; built from an unmounted engine's routes it
215
+ resolves to nothing, and nothing downstream can detect that afterwards
216
+ because the digest is over the wrong link. Presenting now refuses at build
217
+ time with the mount line in the sentence, `bin/rails clickwrap:doctor`
218
+ reports the missing mount as a problem whenever any policy is compiled (it
219
+ used to speak only about `requires_clickwrap` gates), and a non-interactive
220
+ install mounts the engine and says so instead of silently taking the `[y/N]`
221
+ default — `--skip-routes` is the one flag that declines.
222
+
223
+ ### Added — the dream-API pass (driven by installing the gem into a second real application)
224
+
225
+ Two real installations wrote about a hundred lines of nearly identical glue
226
+ around this gem. That is the strongest possible evidence that the code belongs
227
+ in the gem, so each ceremony became behavior, working backwards from the API we
228
+ wished we had written.
229
+
230
+ - **`version:` is optional when the source names its own.** A document declared
231
+ `from:` a file — or with inline `content:` — whose leading YAML front matter
232
+ carries `clickwrap_version:` or `last_updated:` resolves its version label
233
+ from there, so the file that *is* the legal text also names its version and
234
+ there is no second copy of the label anywhere to drift. `clickwrap_version:`
235
+ outranks `last_updated:` on purpose: a same-day correction that still changes
236
+ bytes needs a fresh label while the date readers see stays put. Both host
237
+ applications had written the same front-matter reader module to do this
238
+ themselves; that module stops existing. The old failure mode was a label
239
+ living in `config/clickwrap.rb` and words living in a file, changed in
240
+ separate edits — which either publishes as "same label, different bytes"
241
+ (refused at publish, correctly, but a step later than it needed to be) or
242
+ quietly leaves the label describing text nobody is serving any more. A source
243
+ with no front-matter key and no `version:` is now a boot failure with the fix
244
+ in the sentence, and a `resolver:` source (whose bytes are only read at
245
+ publish time) says exactly why it cannot name its own. Clickwrap never invents a
246
+ label, because a policy that requires a current version cannot be satisfied
247
+ by a guess.
248
+ - **`save`/`save!` pairing for doors and captures.** `register_with_clickwrap`
249
+ (non-bang) absorbs a refused registration — a stale presentation, an unticked
250
+ control, a validation the account failed — into the exact human sentences the
251
+ Devise adapter paints, through one shared `Clickwrap::Registration.absorb_refusal`:
252
+ inline via `clickwrap_errors` beside the control it belongs to, once on the
253
+ record's `:base`, then `false`, ready for `render :new, status: :unprocessable_entity`.
254
+ `capture_clickwrap` and `capture_clickwrap_and` do the same for
255
+ `Clickwrap::CaptureRefused`, leaving the refusal on `clickwrap_refusal` with
256
+ its `user_facing_message`. The old failure mode was every hand-rolled door
257
+ writing its own rescue, each one slightly different, and one of them
258
+ eventually rescuing too much. Infrastructure failures still escape every
259
+ form — `Clickwrap::EventWriteFailed` is not a validation error to dress up —
260
+ and lifecycle conflicts (`ReplayRejected`, `OneTimeAuthorizationConflict`)
261
+ still raise, because "this was already done" needs a domain answer that no
262
+ generic rescue can supply honestly.
263
+ - **`config.document_renderer = :markdown_rails` renders through the
264
+ application's own renderer.** Content-file Rails apps already serve their
265
+ public legal pages through a registered markdown-rails handler; when the same
266
+ files are Clickwrap documents, what people accept and what the page serves
267
+ must be the same bytes — same renderer, same options, no second sanitization
268
+ pass changing entities behind the digest. Wiring that by hand meant a lambda,
269
+ an engine name, gem versions, and a parity test copied between applications;
270
+ this setting says it once, records honest provenance (the renderer class the
271
+ application registered, plus the markdown-rails and Markdown-engine gem
272
+ versions), and resolves the handler lazily at first render so initializer
273
+ order cannot capture the stock default renderer instead of the application's.
274
+ Its bound is stated rather than hidden: the renderer is called without a view
275
+ context, exactly how a frozen snapshot must render, so Markdown that calls
276
+ Rails view helpers fails loudly at publish and needs the explicit lambda form.
277
+ - **Publishing rides `db:prepare`.** The deploy step everyone forgets no longer
278
+ exists: an enhanced `db:prepare` publishes declared documents, so by the time
279
+ the server takes traffic every declared version has an immutable snapshot.
280
+ The old failure mode was the worst kind of quiet — presentations refuse
281
+ unpublished documents (the safe failure), so a forgotten `clickwrap:publish`
282
+ became "nobody can sign up" some hours after a deploy that looked green. It
283
+ is idempotent, silent when no documents are declared, and a sentence rather
284
+ than a crash when the tables are not migrated yet; a real refusal fails the
285
+ deploy out loud, which beats signups failing quietly later. Opt out with
286
+ `config.publish_documents_after_database_preparation = false`.
287
+ - **`config.hotwire_native_document_links` answers the native question once.**
288
+ One declarative seam does both halves coherently — the signed href
289
+ (absolutized against a validated `https` canonical host, which may be a
290
+ callable) and the navigation attributes (`target="_blank"`,
291
+ `rel="noopener"`, `data-turbo="false"` for `:external_browser`; a plain link
292
+ for `:same_screen`). It exists for one specific failure: on a native
293
+ authentication sheet, a same-host document link is routed by the app itself,
294
+ which pops the sheet and takes the half-filled signup form with it. Getting
295
+ the href right in one place and the attributes right in another is exactly
296
+ the kind of split that drifts. When set it answers native renders entirely;
297
+ `config.document_link_html_options_with` goes on answering everything else,
298
+ so an app needing different answers per screen keeps the per-request lambda.
299
+ - **The default `describe_authentication_with` no longer over-claims.** It now
300
+ reports `{ method: :authenticated_session }` only when the controller's
301
+ configured current-actor method actually returns someone, and `{}` otherwise.
302
+ A signup form is not an authenticated session, and describing every request
303
+ as authenticated merely because it passed through `ApplicationController` put
304
+ a claim in the receipt that nobody had checked. It is deliberately gentler
305
+ than the capture path's actor resolution: describing authentication is
306
+ context, not identity, so a controller with no such method is `{}`, never an
307
+ error.
308
+
309
+ ### Added — installer improvements (driven by installing the gem into a template application)
310
+
311
+ - **Non-interactive runs skip the questions instead of jumbling them.** When
312
+ `rails generate clickwrap:install` runs without a terminal (a piped or
313
+ scripted run, CI, an AI agent), it now announces that it detected a
314
+ non-interactive run and takes the same collect-nothing defaults as
315
+ `--skip-questions`, rather than streaming interactive prompts into a pipe
316
+ that would have taken every `[y/N]` default anyway.
317
+ - **Existing legal pages become the document source.** When the host already
318
+ keeps both legal documents at a recognized convention — Sitepress-style
319
+ `app/content/pages/legal/terms.html.md` + `privacy.html.md`, or a previous
320
+ install's `app/content/legal/*.md` — the generated `config/clickwrap.rb`
321
+ points its `from:` lines at those exact files and no placeholders are
322
+ written. What people accept and what the public legal routes render must be
323
+ the same bytes, and a second copy is how they silently stop being the same
324
+ document. Placeholders are still written when no convention matches, and a
325
+ convention only counts when both of its documents exist.
326
+
327
+ ### Added — the hardening pass (driven by an independent adversarial audit of the first production integration)
328
+
329
+ - **Policy-declared tenant semantics: `tenant_is :not_applicable | :optional | :required`.**
330
+ Presentation, capture, verification, and import all resolve the tenant
331
+ through the policy's declaration, and the resolved tenant is signed into
332
+ the presentation manifest — so a personal policy can never inherit an
333
+ ambient organization from the session, and an organization member can no
334
+ longer be dead-ended by a `presentation_tenant_mismatch` between a
335
+ tenant-less render and a tenant-injecting submit. Withdrawal matches each
336
+ grant under its own policy's semantics, so consent granted personally stays
337
+ withdrawable after the person joins an organization; exemptions record the
338
+ canonical tenant reference. Declare `tenant_is :not_applicable` explicitly
339
+ on personal policies — it is the difference between "this evidence never
340
+ changes identity" and "whatever organization happened to be current."
341
+ - **Import chronology and revision honesty.** An imported historical grant
342
+ can never replace a newer live state (effective-time-first comparison, with
343
+ server recording order as tie-breaker only); imported evidence carries an
344
+ explicit legacy-import revision identity that can never satisfy
345
+ `require_current_revision: true`; and **`counts_as_current: false`**
346
+ quarantines an import — it satisfies no predicate, survives projection
347
+ REBUILDS quarantined, and exists for exactly the case where counsel has not
348
+ yet blessed a legacy source (a bundled checkbox, an unverified column).
349
+ Import idempotency covers statement mappings and provenance.
350
+ - **Serialized evidence writes.** Every state-mutating path (capture, import,
351
+ exemption, lifecycle transitions, provider outcomes, projection rebuild)
352
+ takes a per-actor identity lock first and the chain head second, in one
353
+ documented order, so concurrent writers block instead of deadlocking and
354
+ lost-update races on statement states are gone. The concurrency lane runs
355
+ on PostgreSQL and MySQL — SQLite cannot express row locks.
356
+ - **`recorded_after?` is off ULIDs.** Ordering questions are answered by a
357
+ durable database sequence under a unique chain position; ULID comparison —
358
+ process-local monotonicity — is no longer presented as an ordering
359
+ guarantee anywhere. The sequence is the installation's PRIVATE order: it is
360
+ deliberately absent from canonical bodies and receipts, because publishing
361
+ a global counter would hand every receipt holder an enumerable census of
362
+ installation activity.
363
+ - **Atomic protected outcomes.** `record_protected_outcome_with:` on a
364
+ one-time authorization records the exact result of the protected action —
365
+ built via `Clickwrap.protected_outcome`, digest-covered, committed in the
366
+ same transaction, refused on recorder-version drift — so "this evidence
367
+ authorized this exact operation" names the operation's amount, record, and
368
+ state instead of implying them.
369
+ - **Immutable document navigation.** The signed manifest binds each
370
+ statement's immutable document path; choice/radio statements render their
371
+ document links (previously only checkboxes did); and
372
+ `config.document_link_html_options_with` lets hosts choose HOW links open
373
+ (evaluated in the rendering view, so `hotwire_native_app?` works) while the
374
+ href itself is refused under any capitalization. The "opens in a new tab"
375
+ hint renders only when the link actually does.
376
+ - **Engine route authorization.** Receipts and withdrawal routes require a
377
+ present actor before the host callback runs, and the generated
378
+ authorization example now guards `nil == nil` explicitly. Remediation
379
+ tokens CARRY their signed tenant into presentation and capture instead of
380
+ comparing it against an ambient value the engine's routes cannot have.
381
+ - **Evidence-contract model links.** `has_clickwrap_evidence` takes the full
382
+ contract (`policy:`, `statement:`, `actor:`, `subject:`, optional
383
+ `tenant:`/`represented_party:`/`required_for_new_records:`), validates a
384
+ linked event against it, refuses link replacement, and supports model-first
385
+ deployments (inert until the column exists; strict from then on).
386
+ - **Submission hardening.** Forged envelope fields raise instead of being
387
+ dropped; answers are bounded at `Submission::MAX_ANSWER_LENGTH` characters
388
+ and refused — never truncated — beyond it (an answer is a checkbox state or
389
+ a declared choice name, not free text).
390
+ - Atomic represented-party creation (`create_represented_party_with_clickwrap`
391
+ and `including_when_this_action_creates_the_organization:`), atomic local
392
+ projections for external actions, request-aware geolocation provenance, and
393
+ pending-receipt answer readers (`answer_for`, `answered?`, `granted?`,
394
+ `declined?`) that read the validated event being committed instead of
395
+ re-parsing browser params.
396
+
397
+ Changes driven by the first production host application:
398
+
399
+ ### Changed
400
+
401
+ - **Imported legacy evidence now satisfies the everyday predicates.**
402
+ `Clickwrap.import_legacy!` projects into current state exactly as a capture
403
+ does, so `agreed_to?`, `acknowledged?`, and `current_for?` keep answering
404
+ what the source system answered — a migration no longer implies mass forced
405
+ re-acceptance. Provenance is unchanged (`imported_legacy` event type,
406
+ `imported_provider` attribution, unknowns named in the receipt), and
407
+ `require_current_version: true` still re-prompts when documents move on.
408
+ 0.1.0 was never published, so no installed application observes a behavior
409
+ change.
410
+ - The Devise adapter refuses a submission with a missing/stale presentation or
411
+ a declined required statement on the re-rendered form, with the gem's
412
+ localized user-facing sentences (inline beside the control and on `:base`) —
413
+ never a raw exception or a developer-facing message.
414
+ - The framework-integration modules (`FormBuilderExtensions`,
415
+ `ControllerHelpers`, `Registration`, `DocumentRenderers::Markdown`) are
416
+ required at boot instead of autoloaded, so hosts whose other gems load
417
+ Action View/Action Controller first no longer fail with an uninitialized
418
+ constant.
419
+ - The install migration recognizes the PostGIS adapter as PostgreSQL (jsonb)
420
+ and Trilogy as MySQL; the generated initializer selects the `:safe_text`
421
+ renderer explicitly instead of writing `nil` (which disabled rendering).
422
+
423
+ ### Added
424
+
425
+ - **Public forms that find or create their record:**
426
+ `register!(..., actor_may_already_exist: true)`. The lead-capture /
427
+ newsletter shape — an anonymous visitor submits a public form and the host
428
+ resolves the row by typed email — is one `register_with_clickwrap` call for
429
+ both cases now. When the submission created the row, attribution stays
430
+ `account_registration`; when it matched an existing row, the receipt
431
+ records the new `public_form` attribution instead, because no account was
432
+ created by the act. Without the explicit option, a persisted prospective
433
+ actor is still refused (that is usually a bug — the host meant `capture!`
434
+ with `actor:`), and the refusal teaches the option. (Forced by migrating a
435
+ production lead-magnet funnel whose leads upsert by email.)
436
+ - **`Clickwrap::CaptureRefused` with `#user_facing_message`.** Every refusal a
437
+ person can cause from a form — a stale or missing presentation, an
438
+ unparseable submission, a declined required statement — now shares one
439
+ exception superclass carrying a localized sentence fit to show them, so a
440
+ host controller handles the whole family in one rescue:
441
+ `rescue Clickwrap::CaptureRefused => refusal; redirect_to ..., alert:
442
+ refusal.user_facing_message`. Infrastructure failures stay outside the
443
+ family and stay loud. (Extracted from a real host that had grown five
444
+ hand-written rescue sites for the same distinctions.)
445
+ - **`has_clickwrap_evidence` + `bin/rails generate clickwrap:link TABLE`.**
446
+ One macro and one generator for the row-level link between a domain record
447
+ and the capture that authorized it: the generator writes the
448
+ `clickwrap_event_id` column migration (ULID string, indexed, nullable, no
449
+ foreign key — each deliberate, and the migration says why), and the macro
450
+ gives the model `clickwrap_event` and `clickwrap_receipt`, so
451
+ `withdrawal.clickwrap_receipt.verify` is one line years later.
452
+ - **`Result#recorded_after?(other)`** on verification results — enforce
453
+ evidence ordering ("the declaration must postdate the preparation") without
454
+ hosts comparing event ids by hand; accepts another result or a bare event
455
+ id and is false whenever either side is missing.
456
+ - The installer's post-install checklist now includes the test-suite setup:
457
+ include `Clickwrap::TestHelpers`, publish once per parallel worker and once
458
+ per process, and read submissions off rendered pages with
459
+ `clickwrap_params_from` — presentation tokens are signed and session-bound,
460
+ so tests cannot fabricate them by hand (that is the point).
461
+ - **[Integrating guide](guides/integrating.md)** — the battle-tested playbook
462
+ from migrating a production application onto the gem end to end: install
463
+ order, pointing documents at real legal content, test setup, Devise
464
+ dual-write bridges, custom surfaces, money-path protection, legacy import,
465
+ request-evidence enablement, and the dual-write → dual-belt → retire
466
+ rollout doctrine.
467
+ - **View helpers for custom surfaces** (`Clickwrap::ViewHelpers`, available in
468
+ every view): `clickwrap_presentation_token_field`,
469
+ `clickwrap_statement_check_box`, `clickwrap_statement_radio_button`, and
470
+ `clickwrap_submit_button` own the three contracts a hand-written form gets
471
+ wrong silently — the envelope name, the statement control names/ids, and a
472
+ call to action worded by the signed manifest itself. The host owns every
473
+ class and wrapper around them. (Extracted from the first production host's money-path
474
+ migration, where each custom form repeated all three by hand.)
475
+ - **`Clickwrap.verify(..., require_current_revision: true)`** — opt-in
476
+ revision currency: evidence recorded under a superseded policy revision
477
+ fails with `:stale_policy_revision` (the verify-time counterpart of the
478
+ capture-time symbol), so "legal reworded the statement → re-ask" is one
479
+ keyword instead of a hand-rolled revision comparison at the host's service
480
+ boundary.
481
+ - **Predicates on verification results**, one per stable error symbol and
482
+ generated from the vocabulary so they can never drift:
483
+ `result.no_evidence?`, `result.subject_fingerprint_mismatch?`,
484
+ `result.stale_policy_revision?`, …
485
+ - `clickwrap_params_from(path, answers: {})` in TestHelpers — the one-line
486
+ integration-test pattern: GET the page, read the signed token and controls
487
+ back off it, return the POST params.
488
+ - The unknown-document boot error now mentions `document: nil` for statements
489
+ about operational facts with no published document.
490
+ - `config.document_renderer = :markdown` — real HTML through whichever
491
+ Markdown library the host already bundles (commonmarker, redcarpet, or
492
+ kramdown; no new dependency), with leading YAML front matter stripped from
493
+ the rendered representation only, the engine name and version recorded in
494
+ the receipt, and the same safe-list sanitizer as the reference renderer.
495
+ - Spanish locale (`config/locales/es.yml`).
496
+ - `clickwrap_submission_params_from(response)` test helper: host integration
497
+ tests read the signed presentation token and its controls back off the
498
+ rendered page, the way a browser does.
499
+
500
+ ## [0.1.0] - 2026-08-15
501
+
502
+ First implemented release. `clickwrap` turns terms acceptance, privacy notice
503
+ acknowledgment, consent, factual declarations, operator attestations, and
504
+ one-time authorizations into one Rails primitive: immutable versioned
505
+ documents, server-owned policies, signed presentation manifests, append-oriented
506
+ evidence events with fixed named disposition transitions, and canonical receipts that can be checked without the
507
+ application that wrote them. Required evidence and the protected database
508
+ action commit in the same transaction, so an account, payout, or handoff cannot
509
+ succeed without the evidence that authorized it. Request evidence — IP address,
510
+ browser user-agent, IP geolocation — stays off until a policy names the field,
511
+ its purpose, and its retention. The gem provides evidence mechanics only: your
512
+ application and its counsel still own the legal text, lawful basis, substantive
513
+ validity, capacity, authority, and retention periods.
514
+
515
+ ### Added
516
+
517
+ - **Immutable versioned documents.** `Clickwrap.document :terms, version:, from:`
518
+ points at the files your application already owns; `bin/rails clickwrap:publish`
519
+ freezes each version into a snapshot with a versioned SHA-256 digest of the
520
+ exact bytes, and `clickwrap:publish:plan` previews what a boot would publish.
521
+ A published version is never rewritten — editing the source file is a new
522
+ version, and receipts keep resolving the bytes they were captured against.
523
+ - **Server-owned compiled policies with six honest kinds.** `Clickwrap.policy`
524
+ and its verbal DSL — `agree_to` (agreement), `acknowledge` (acknowledgment),
525
+ `consent_to` (consent), `declare` (declaration), `attest` (attestation),
526
+ `authorize` (authorization) — give each act the lifecycle it actually needs
527
+ instead of calling every checkbox "consent." The policy compiler runs at boot
528
+ and refuses incoherent combinations in a full sentence: an indefinite one-time
529
+ authorization, consent without a withdrawal path, an expiring declaration that
530
+ would have to pretend the original statement was false. The taxonomy is
531
+ product design, not statutory vocabulary; the host picks the kind.
532
+ - **Signed presentation manifests.** Every rendered policy carries a signed,
533
+ short-lived manifest of exactly what the server generated and offered: policy key and
534
+ revision, document versions and digests, assertion and link text, choices,
535
+ submit-button text, and locale. Submission is validated against that manifest
536
+ and rechecked server-side, so render-to-submit substitution — a different
537
+ version, a different call to action, a checkbox the server never required —
538
+ is rejected rather than recorded. The browser may answer a policy; it can
539
+ never choose the policy, the version, the validity window, the subject, the
540
+ retention class, or a request-evidence field.
541
+ - **`capture!`, `capture_and!`, and `register!` with same-transaction atomicity.**
542
+ `capture_and!` yields a read-only `Clickwrap::PendingReceipt` whose stable
543
+ `event_id` the domain row can store, and commits the evidence and the
544
+ protected action together or not at all; if the transaction rolls back the
545
+ pending object becomes invalid instead of masquerading as committed evidence.
546
+ `capture!` records evidence on its own, and `register!` binds a prospective
547
+ actor to the presentation that preceded the account — the Rails-authentication
548
+ and Devise adapters are thin conveniences over it. Optional
549
+ `after_event_is_committed` hooks are error-isolated and can never undo a
550
+ committed action or stand in for one.
551
+ - **Lifecycles that stay truthful over time.** Consent can actually be
552
+ withdrawn, renewed, and scope-changed; declarations expire, get corrected, and
553
+ get superseded without rewriting what was originally stated; one-time
554
+ authorizations are locked and consumed inside the same transaction as the
555
+ action they authorize, so a stale token, a changed subject, a wrong ordering,
556
+ or a concurrent replay cannot reuse one. Every ordinary lifecycle transition
557
+ appends an event with its own predecessor link; reviewed retention uses the
558
+ separately named, fixed disposition transition rather than masquerading as an
559
+ ordinary append.
560
+ - **Canonical receipts and a standalone verifier.** Receipts use versioned
561
+ schemas serialized with the [JSON Canonicalization Scheme (RFC 8785)](https://www.rfc-editor.org/rfc/rfc8785)
562
+ plus a published Clickwrap profile for UTC timestamps, decimals, identifiers,
563
+ binary digests, absent values, and extension names — never Ruby object
564
+ serialization, YAML, or database column order. `bin/rails clickwrap:verify`
565
+ and `clickwrap:export` produce and check bundles without the host
566
+ application's source code, and golden fixtures pin every released format so
567
+ new versions keep verifying old receipts. An unknown schema version fails
568
+ honestly instead of being reinterpreted. The baseline tier verifies schema,
569
+ canonical bytes, digests, links, and bundled content consistency, and says so
570
+ precisely; it claims nothing about origin or time that it cannot show.
571
+ - **Optional request evidence, off by default and encrypted.** IP address,
572
+ browser user-agent, and each individual IP-geolocation field are collected
573
+ only when a policy names them with a plain-English purpose and a retention
574
+ decision. They live in a separate `ActiveRecord::Encryption` annex, are read
575
+ through their own authorization callback, and are separately disposable
576
+ without touching the core event. There is no `gdpr_compliant_mode`,
577
+ `full_evidence`, or `legal_proof` switch that turns on a category of personal
578
+ data as a side effect of something else — the installer's recipes write every
579
+ individual setting into the initializer and then disappear.
580
+ - **Retention classes, legal holds, and dry-run disposition.**
581
+ `Clickwrap.retention` expresses per-field retention (including event-based and
582
+ "later of" rules) as executable, auditable policy. `clickwrap:retention:plan`
583
+ produces an immutable, scoped, expiring plan that `clickwrap:retention:apply`
584
+ rechecks before touching anything, so a newly placed hold or a changed policy
585
+ stops disposition instead of deleting more than the operator reviewed.
586
+ `place_on_legal_hold!` / `release_legal_hold!` require a reason, an owner, and
587
+ a review date; placing and releasing a hold append corresponding evidence
588
+ events while the hold row remains an explicit current-state record. Destructive methods
589
+ name exactly what they remove (`delete_recorded_ip_address!`,
590
+ `delete_recorded_browser_user_agent!`, `delete_recorded_ip_geolocation!`) and
591
+ append a disposition event rather than rewriting history. Clickwrap does not
592
+ decide retention periods; it makes reviewed ones executable.
593
+ - **Generators for every step.** `clickwrap:install` detects integer versus UUID
594
+ keys, the database adapter, and Rails authentication versus Devise; it stops
595
+ and explains itself when the actor or tenant mapping is ambiguous, asks
596
+ separately about every request-evidence field, and prints a post-install
597
+ checklist. `clickwrap:policy`, `clickwrap:document`, `clickwrap:views`,
598
+ `clickwrap:hardening --database`, and `clickwrap:upgrade` cover the rest.
599
+ Upgrade generators always create new migrations; a released migration is never
600
+ silently edited underneath an installed application.
601
+ - **Importers that do not invent history.** `clickwrap:import:fine_print:plan` /
602
+ `clickwrap:import:fine_print` turn FinePrint contract versions and signatures
603
+ into explicit `imported_legacy` events, and `Clickwrap.import_legacy!` does the
604
+ same for an `accepted_terms_at` column. Fields the legacy source never
605
+ recorded — presentation manifest, IP address, call to action, protected
606
+ action — stay `unknown` or `not_collected`. Clickwrap never synthesizes
607
+ evidence it does not have.
608
+ - **A form-builder helper and ejectable reference views.**
609
+ `form.clickwrap :signup, submit: "Create account"` renders the initially
610
+ unselected controls and the bound submit button as one presentation, so the
611
+ call to action in the signed manifest is the one the user can actually press.
612
+ `rails generate clickwrap:views` ejects the reference views — including the
613
+ standalone remediation screen — for hosts that want their own markup.
614
+
3
615
  ## [0.0.0]
4
616
 
5
617
  - Name-reservation release. No implementation: no engine, no models, no