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.
- checksums.yaml +4 -4
- data/.rubocop.yml +90 -0
- data/CHANGELOG.md +612 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +241 -0
- data/app/controllers/clickwrap/application_controller.rb +79 -0
- data/app/controllers/clickwrap/captures_controller.rb +145 -0
- data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
- data/app/controllers/clickwrap/receipts_controller.rb +115 -0
- data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
- data/app/helpers/clickwrap/engine_helper.rb +97 -0
- data/app/views/clickwrap/captures/show.html.erb +34 -0
- data/app/views/clickwrap/receipts/index.html.erb +38 -0
- data/app/views/clickwrap/receipts/show.html.erb +91 -0
- data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
- data/app/views/clickwrap/shared/_fields.html.erb +100 -0
- data/app/views/clickwrap/shared/_statement.html.erb +105 -0
- data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
- data/config/locales/en.yml +160 -0
- data/config/locales/es.yml +138 -0
- data/config/routes.rb +41 -0
- data/exe/clickwrap +374 -0
- data/guides/README.md +30 -0
- data/guides/accessibility.md +249 -0
- data/guides/consent-and-lifecycle.md +299 -0
- data/guides/integrating.md +610 -0
- data/guides/integrity.md +212 -0
- data/guides/migrating.md +335 -0
- data/guides/naming.md +320 -0
- data/guides/organizations.md +320 -0
- data/guides/receipts-and-verification.md +415 -0
- data/guides/request-evidence.md +512 -0
- data/guides/retention-and-legal-holds.md +438 -0
- data/lib/clickwrap/actor_proxy.rb +147 -0
- data/lib/clickwrap/anonymous_actor.rb +47 -0
- data/lib/clickwrap/authority.rb +174 -0
- data/lib/clickwrap/canonical_json.rb +216 -0
- data/lib/clickwrap/capture/event_builder.rb +220 -0
- data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
- data/lib/clickwrap/capture.rb +650 -0
- data/lib/clickwrap/configuration.rb +1129 -0
- data/lib/clickwrap/controller_helpers.rb +758 -0
- data/lib/clickwrap/current_state.rb +282 -0
- data/lib/clickwrap/digest.rb +125 -0
- data/lib/clickwrap/doctor.rb +418 -0
- data/lib/clickwrap/document_definition.rb +255 -0
- data/lib/clickwrap/document_renderer.rb +83 -0
- data/lib/clickwrap/document_renderers/markdown.rb +175 -0
- data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
- data/lib/clickwrap/dsl/policy_builder.rb +462 -0
- data/lib/clickwrap/dsl/retention_builder.rb +89 -0
- data/lib/clickwrap/durable_commit_callback.rb +37 -0
- data/lib/clickwrap/engine.rb +184 -0
- data/lib/clickwrap/errors.rb +181 -0
- data/lib/clickwrap/form_builder_extensions.rb +341 -0
- data/lib/clickwrap/front_matter.rb +67 -0
- data/lib/clickwrap/identifier.rb +112 -0
- data/lib/clickwrap/import/external_receipt.rb +241 -0
- data/lib/clickwrap/import/fine_print.rb +290 -0
- data/lib/clickwrap/import/legacy.rb +450 -0
- data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
- data/lib/clickwrap/integrity/anchor.rb +130 -0
- data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
- data/lib/clickwrap/integrity/attestor.rb +221 -0
- data/lib/clickwrap/integrity/chain.rb +313 -0
- data/lib/clickwrap/integrity/timestamp.rb +143 -0
- data/lib/clickwrap/ip_geolocation/location.rb +112 -0
- data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
- data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
- data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
- data/lib/clickwrap/ip_geolocation.rb +16 -0
- data/lib/clickwrap/lifecycle.rb +534 -0
- data/lib/clickwrap/linter.rb +382 -0
- data/lib/clickwrap/localized_text.rb +101 -0
- data/lib/clickwrap/macros.rb +203 -0
- data/lib/clickwrap/models/application_record.rb +20 -0
- data/lib/clickwrap/models/chain_head.rb +79 -0
- data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
- data/lib/clickwrap/models/disposition_plan.rb +208 -0
- data/lib/clickwrap/models/document.rb +46 -0
- data/lib/clickwrap/models/document_version.rb +163 -0
- data/lib/clickwrap/models/event.rb +743 -0
- data/lib/clickwrap/models/event_document.rb +79 -0
- data/lib/clickwrap/models/event_statement.rb +92 -0
- data/lib/clickwrap/models/external_action.rb +150 -0
- data/lib/clickwrap/models/integrity_attestation.rb +90 -0
- data/lib/clickwrap/models/legal_hold.rb +81 -0
- data/lib/clickwrap/models/policy_revision.rb +115 -0
- data/lib/clickwrap/models/presentation.rb +59 -0
- data/lib/clickwrap/models/receipt_access.rb +53 -0
- data/lib/clickwrap/models/recording_sequence.rb +21 -0
- data/lib/clickwrap/models/request_evidence.rb +378 -0
- data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
- data/lib/clickwrap/models/statement_state.rb +130 -0
- data/lib/clickwrap/pending_receipt.rb +177 -0
- data/lib/clickwrap/policy.rb +283 -0
- data/lib/clickwrap/presentation_manifest.rb +210 -0
- data/lib/clickwrap/presenter.rb +716 -0
- data/lib/clickwrap/privacy.rb +419 -0
- data/lib/clickwrap/protected_outcome.rb +120 -0
- data/lib/clickwrap/receipt.rb +606 -0
- data/lib/clickwrap/receipt_html.rb +235 -0
- data/lib/clickwrap/receipt_verifier.rb +978 -0
- data/lib/clickwrap/reference.rb +44 -0
- data/lib/clickwrap/registration.rb +236 -0
- data/lib/clickwrap/registry.rb +54 -0
- data/lib/clickwrap/remediation_token.rb +155 -0
- data/lib/clickwrap/request_evidence_extractor.rb +590 -0
- data/lib/clickwrap/request_evidence_policy.rb +261 -0
- data/lib/clickwrap/retention/applier.rb +231 -0
- data/lib/clickwrap/retention/disposition.rb +221 -0
- data/lib/clickwrap/retention/planner.rb +502 -0
- data/lib/clickwrap/retention_class.rb +97 -0
- data/lib/clickwrap/reviewed_text.rb +28 -0
- data/lib/clickwrap/schema_requirements.rb +196 -0
- data/lib/clickwrap/services/authorize_external_action.rb +149 -0
- data/lib/clickwrap/services/load_policies.rb +69 -0
- data/lib/clickwrap/services/publish_documents.rb +251 -0
- data/lib/clickwrap/services/validate_policy_references.rb +166 -0
- data/lib/clickwrap/statement.rb +248 -0
- data/lib/clickwrap/subject_fingerprint.rb +28 -0
- data/lib/clickwrap/submission.rb +169 -0
- data/lib/clickwrap/system_actor.rb +31 -0
- data/lib/clickwrap/test_helpers.rb +676 -0
- data/lib/clickwrap/testing.rb +211 -0
- data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
- data/lib/clickwrap/verification.rb +504 -0
- data/lib/clickwrap/version.rb +12 -1
- data/lib/clickwrap/view_helpers.rb +190 -0
- data/lib/clickwrap/vocabulary.rb +294 -0
- data/lib/clickwrap.rb +497 -7
- data/lib/generators/clickwrap/document_generator.rb +164 -0
- data/lib/generators/clickwrap/hardening_generator.rb +177 -0
- data/lib/generators/clickwrap/install_generator.rb +1287 -0
- data/lib/generators/clickwrap/link_generator.rb +56 -0
- data/lib/generators/clickwrap/policy_generator.rb +118 -0
- data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
- data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
- data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
- data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
- data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
- data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
- data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
- data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
- data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
- data/lib/generators/clickwrap/views_generator.rb +101 -0
- data/lib/tasks/clickwrap.rake +569 -0
- 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
|