clickwrap 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +90 -0
- data/CHANGELOG.md +650 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +245 -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 +154 -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 +187 -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 +729 -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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Consent, kinds, and lifecycle
|
|
2
|
+
|
|
3
|
+
Not every checkbox is consent, and not every timestamp is a signature. Clickwrap gives each act
|
|
4
|
+
the lifecycle it actually needs, which is why there are six verbs rather than one
|
|
5
|
+
`accept` method with options.
|
|
6
|
+
|
|
7
|
+
**This taxonomy is product design, not statutory vocabulary.** Naming an act `agreement` or
|
|
8
|
+
`consent` does not determine its legal effect anywhere. The verb decides which lifecycle
|
|
9
|
+
Clickwrap enforces and what the receipt says; your application and its counsel decide whether
|
|
10
|
+
that is the right characterization, what the words mean, and what basis the processing rests
|
|
11
|
+
on. Choosing between the verbs is the one piece of thinking this gem deliberately does not do
|
|
12
|
+
for you.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The six kinds
|
|
17
|
+
|
|
18
|
+
| Verb | Kind | Means | First action | Every action it can ever record |
|
|
19
|
+
|---|---|---|---|---|
|
|
20
|
+
| `agree_to` | `agreement` | Assent to contractual terms | `agreed` | `agreed`, `superseded` |
|
|
21
|
+
| `acknowledge` | `acknowledgment` | Affirmative receipt or awareness of a notice or risk | `acknowledged` | `acknowledged`, `superseded`, `expired` |
|
|
22
|
+
| `consent_to` | `consent` | Purpose-specific permission, where the host has decided consent is the right basis | `granted` | `granted`, `declined`, `withdrawn`, `renewed`, `scope_changed` |
|
|
23
|
+
| `declare` | `declaration` | A factual statement made by the actor | `declared` | `declared`, `corrected`, `superseded`, `expired` |
|
|
24
|
+
| `attest` | `attestation` | An operational fact affirmed by an authorized actor, usually an operator | `attested` | `attested`, `corrected`, `superseded` |
|
|
25
|
+
| `authorize` | `authorization` | Narrow permission bound to one protected action | `authorized` | `authorized`, `consumed`, `expired`, `revoked` |
|
|
26
|
+
|
|
27
|
+
The action lists are not suggestions. `EventStatement` validates that an action belongs to its
|
|
28
|
+
kind and refuses the row otherwise, with a message naming what that kind can record. An
|
|
29
|
+
agreement cannot be `withdrawn`; a declaration cannot be `consumed`.
|
|
30
|
+
|
|
31
|
+
### Which capability each kind has, and why
|
|
32
|
+
|
|
33
|
+
| Capability | Kinds that have it | Why the others do not |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| **Withdrawable** | `consent` only | Withdrawing future consent must never rewrite a historical agreement or a factual declaration. Those are statements about what happened; only a permission can be taken back |
|
|
36
|
+
| **Expirable** (`valid_for:`) | `acknowledgment`, `consent`, `declaration`, `authorization` | An agreement does not lapse on a timer — it is superseded by a new version. An attestation is an operational fact that stays true about the moment it described |
|
|
37
|
+
| **Correctable** (`correct_declaration!`) | `declaration`, `attestation` | A correction says the fact is different now, not that the original statement was false when it was made. Only statements of fact have that shape |
|
|
38
|
+
| **One-time** (`one_time: true`) | `authorization` only | Scoping to a single protected action is what makes an authorization an authorization |
|
|
39
|
+
|
|
40
|
+
The policy compiler rejects incoherent combinations at boot rather than at 3am: a one-time
|
|
41
|
+
authorization that is also indefinite, a consent with no withdrawal path, a subject-bound
|
|
42
|
+
statement with no subject fingerprint, a request-evidence field with no purpose or retention
|
|
43
|
+
decision.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## States and how they change
|
|
48
|
+
|
|
49
|
+
Current state is a **projection** derived from retained event payloads.
|
|
50
|
+
`CurrentState.rebuild_for!(actor_reference:)` replays those payloads while they still contain
|
|
51
|
+
the affected statement identity. A reviewed core disposition deliberately removes that personal
|
|
52
|
+
identity; after that point the method refuses to delete an existing projection that depends on
|
|
53
|
+
the disposed root. Do not drop the projection after core disposition and expect removed identity
|
|
54
|
+
facts to be reconstructed. Keeping it downstream of retained evidence is what lets it be mutable
|
|
55
|
+
and fast without treating the projection itself as the evidence record.
|
|
56
|
+
|
|
57
|
+
| State | Satisfies a requirement | How a statement gets there |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `active` | Yes | A `capture`, `renewal`, or `scope_change` event recorded the initial action |
|
|
60
|
+
| `declined` | No | The person answered no to a statement with explicit `choices:` |
|
|
61
|
+
| `withdrawn` | No | `Clickwrap.withdraw!` appended a `withdrawal` event |
|
|
62
|
+
| `expired` | No | The validity period ran out. Evaluated live against the clock, so verification does not depend on a job having run |
|
|
63
|
+
| `superseded` | No | A newer act for the same identity replaced it, or `Clickwrap.supersede!` appended a `supersession` event |
|
|
64
|
+
| `consumed` | No | A one-time authorization was used by the transaction it authorized |
|
|
65
|
+
| `revoked` | No | `Clickwrap.revoke!` appended a `revocation` event |
|
|
66
|
+
| `corrected` | No | `Clickwrap.correct_declaration!` appended a `correction` event; the correction becomes the active statement |
|
|
67
|
+
| `exempted` | Only `exempted_from?` | `Clickwrap.exempt!` recorded that no human acted |
|
|
68
|
+
|
|
69
|
+
### The transition table
|
|
70
|
+
|
|
71
|
+
| From | Event type | Action recorded | To | What is preserved |
|
|
72
|
+
|---|---|---|---|---|
|
|
73
|
+
| — | `capture` | the kind's initial action | `active` | Everything: the exact assertion, documents, manifest, answers |
|
|
74
|
+
| `active` | `capture` (newer act, same identity) | the kind's initial action | previous becomes `superseded` | The earlier event, untouched. The projection moves on; the event does not |
|
|
75
|
+
| `active` | `withdrawal` | `withdrawn` | `withdrawn` | The original grant. Withdrawal appends; it never deletes or mutates |
|
|
76
|
+
| `active` | `renewal` | `renewed` | `active`, with a **fresh** validity period | The old expiry does not quietly survive: `effective_at` moves, `withdrawn_at` clears, `expires_at` is recomputed |
|
|
77
|
+
| `active` | `scope_change` | `scope_changed` | `active` | The previous scope, as its own event |
|
|
78
|
+
| `active` | `correction` | `corrected` | `corrected`, and the correction is `active` | The original statement, which was not false when it was made |
|
|
79
|
+
| `active` | `supersession` | `superseded` | `superseded` | Everything the superseded act recorded |
|
|
80
|
+
| `active` | `consumption` | `consumed` | `consumed` | The binding between this evidence and the exact transaction it authorized |
|
|
81
|
+
| `active` | `revocation` | `revoked` | `revoked` | The original authorization |
|
|
82
|
+
| `active` | (validity elapses) | `expired` | `expired` | The original act. Expiry does not imply the statement was ever untrue |
|
|
83
|
+
| — | `exemption` | the kind's initial action | `exempted` | Who or what created it, and why. Never satisfies a human-action predicate |
|
|
84
|
+
|
|
85
|
+
Only four event types count as a human action recorded through a Clickwrap presentation:
|
|
86
|
+
`capture`, `correction`, `renewal`, `scope_change`. Only those can satisfy `agreed_to?`,
|
|
87
|
+
`consented_to?`, `declared?`, and the rest. An `exemption`, an `imported_legacy` event, or an
|
|
88
|
+
`external_receipt` never does — there is no "missing checkbox means system account" inference
|
|
89
|
+
anywhere in this gem.
|
|
90
|
+
|
|
91
|
+
The remaining event types record things that happened *to* evidence rather than acts by a
|
|
92
|
+
person: `disposition`, `legal_hold_placed`, `legal_hold_released`, `receipt_access`,
|
|
93
|
+
`provider_outcome`.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Consent specifically
|
|
98
|
+
|
|
99
|
+
Consent is the only withdrawable kind, and Clickwrap structurally requires an accessible
|
|
100
|
+
withdrawal path before it will compile a consent policy. It does not decide whether consent is
|
|
101
|
+
the correct basis for your processing.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Clickwrap.policy :marketing_preferences do
|
|
105
|
+
consent_to :product_updates,
|
|
106
|
+
document: :marketing_notice,
|
|
107
|
+
statement: "I agree to receive product update emails.",
|
|
108
|
+
optional: true,
|
|
109
|
+
withdrawal_path: "/settings/privacy"
|
|
110
|
+
|
|
111
|
+
consent_to :partner_offers,
|
|
112
|
+
document: :marketing_notice,
|
|
113
|
+
statement: "I agree to receive offers from selected partners.",
|
|
114
|
+
optional: true,
|
|
115
|
+
withdrawal_path: "/settings/privacy"
|
|
116
|
+
|
|
117
|
+
retain_with :marketing_consent_evidence
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Two separate purposes, two separate controls, two separate lifecycles. Bundling them into one
|
|
122
|
+
sentence produces a consent record whose meaning nobody can reconstruct later — the development
|
|
123
|
+
linter flags exactly that as `consent_statement_bundles_purposes` when it sees "and", "and/or",
|
|
124
|
+
"as well as", or "plus" in a consent assertion.
|
|
125
|
+
|
|
126
|
+
**An unselected optional control creates no grant.** The receipt can show the option was offered
|
|
127
|
+
and not taken, but it does not call silence an affirmative refusal — those are different facts
|
|
128
|
+
and only one of them happened. When you actually need a recorded decision rather than the
|
|
129
|
+
absence of one, ask for it explicitly:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
consent_to :research_contact,
|
|
133
|
+
choices: { yes: :grant, no: :decline },
|
|
134
|
+
require_an_explicit_choice: true,
|
|
135
|
+
withdrawal_path: "/settings/privacy"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Both controls start unselected, and the answer becomes `granted` or `declined` — a real
|
|
139
|
+
recorded decision either way.
|
|
140
|
+
|
|
141
|
+
Withdrawal appends:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
Clickwrap.withdraw!(
|
|
145
|
+
:product_updates,
|
|
146
|
+
actor: current_user,
|
|
147
|
+
http_request: request,
|
|
148
|
+
because: "The user withdrew this purpose in privacy settings"
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Stopping the downstream processing is your job, and it happens after the transaction commits so
|
|
153
|
+
that the record of the withdrawal never depends on a job backend being up:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
config.after_event_is_committed = lambda do |event|
|
|
157
|
+
if event.event_type == "withdrawal"
|
|
158
|
+
Marketing::StopProcessingJob.perform_later(event.actor_reference)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The hook is an observer, never authorization. A failure here is reported through
|
|
164
|
+
`report_after_commit_failure_with` and swallowed, because the withdrawal event has already
|
|
165
|
+
committed and nothing a job backend does may undo it.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Choosing a verb: Terms, privacy notice, marketing
|
|
170
|
+
|
|
171
|
+
This is the case that could plausibly be any of the three, and getting it wrong is the most
|
|
172
|
+
common way an evidence record ends up saying something that did not happen. One signup page,
|
|
173
|
+
three different acts:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
Clickwrap.policy :signup do
|
|
177
|
+
agree_to :terms
|
|
178
|
+
acknowledge :privacy_notice
|
|
179
|
+
consent_to :product_updates,
|
|
180
|
+
document: :marketing_notice,
|
|
181
|
+
optional: true,
|
|
182
|
+
withdrawal_path: "/settings/privacy"
|
|
183
|
+
|
|
184
|
+
retain_with :ordinary_agreement_evidence
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Terms of service → `agree_to`.** The Terms are a contract you are asking the person to be
|
|
189
|
+
bound by. The act is assent. It does not expire on a timer; it is superseded when a new version
|
|
190
|
+
publishes and you decide the change is material enough to require reacceptance. It is not
|
|
191
|
+
withdrawable — you do not "withdraw" from terms, you stop using the service or the contract
|
|
192
|
+
ends.
|
|
193
|
+
|
|
194
|
+
**Privacy notice → `acknowledge`.** A privacy notice is information the person is *entitled to*
|
|
195
|
+
under transparency duties ([GDPR Article 13](https://eur-lex.europa.eu/eli/reg/2016/679/art_13/oj/eng))
|
|
196
|
+
*(law)*. It is not something they agree to, and it is not permission for anything. Recording it
|
|
197
|
+
as an agreement would claim they assented to processing; recording it as consent would be
|
|
198
|
+
worse, because it would claim they granted a basis they were never asked for.
|
|
199
|
+
|
|
200
|
+
The Spanish supervisory authority puts the distinction plainly: a checkbox may serve to prove
|
|
201
|
+
that the privacy information was provided, while purposes that genuinely rest on consent should
|
|
202
|
+
be collected separately
|
|
203
|
+
([AEPD FAQ 02.48](https://www.aepd.es/preguntas-frecuentes/2-tus-obligaciones-como-responsable-del-tratamiento/6-el-deber-de-informacion/FAQ-0248-sobre-si-el-usuario-tiene-que-dar-consentimiento-a-clausula-de-privacidad))
|
|
204
|
+
*(regulator guidance)*. `acknowledge` is the verb that records the first thing without claiming
|
|
205
|
+
the second.
|
|
206
|
+
|
|
207
|
+
**Marketing email → `consent_to`, optional, with a withdrawal path.** This is a specific
|
|
208
|
+
purpose, separable from the service, and it is the one thing on the page that is genuinely a
|
|
209
|
+
permission. It is optional, so leaving it alone grants nothing. It is withdrawable, and the
|
|
210
|
+
withdrawal path is required at compile time rather than promised in a policy document.
|
|
211
|
+
|
|
212
|
+
Consent is one lawful basis among several, and where it is used, GDPR requires it to be
|
|
213
|
+
demonstrable, distinguishable from other matters, and withdrawable
|
|
214
|
+
([Article 6](https://eur-lex.europa.eu/eli/reg/2016/679/art_6/oj/eng),
|
|
215
|
+
[Article 7](https://eur-lex.europa.eu/eli/reg/2016/679/art_7/oj/eng)) *(law)*. Clickwrap gives
|
|
216
|
+
you the mechanics for all three. It does not choose the basis, and no verb here should be read
|
|
217
|
+
as advice that you have chosen correctly.
|
|
218
|
+
|
|
219
|
+
### A quick way to decide
|
|
220
|
+
|
|
221
|
+
Ask, in order:
|
|
222
|
+
|
|
223
|
+
1. **Is this a permission the person can take back without ending the relationship?** If yes,
|
|
224
|
+
`consent_to`, with a real withdrawal path. If withdrawing it would just mean cancelling the
|
|
225
|
+
account, it is not consent.
|
|
226
|
+
2. **Is this information they are entitled to receive, rather than something they grant?** If
|
|
227
|
+
yes, `acknowledge`.
|
|
228
|
+
3. **Is this assent to terms that bind them?** If yes, `agree_to`.
|
|
229
|
+
4. **Is it a statement of fact about themselves or the world?** `declare` if the person is
|
|
230
|
+
asserting it, `attest` if an authorized operator is.
|
|
231
|
+
5. **Is it permission for one specific, consequential operation?** `authorize`, with
|
|
232
|
+
`one_time: true` and a validity window.
|
|
233
|
+
|
|
234
|
+
If a single control would need two of these answers, it is two controls.
|
|
235
|
+
|
|
236
|
+
### One control, several acts — and the line consent never crosses
|
|
237
|
+
|
|
238
|
+
The default rendering composes an ordinary `agree_to` and `acknowledge` into one checkbox
|
|
239
|
+
carrying one sentence, and the receipt still records them as two acts with two lifecycles. That
|
|
240
|
+
is not a contradiction of the rule above: the person is not being asked to answer them
|
|
241
|
+
differently, they are being asked to do the two ordinary things a signup asks for, and the
|
|
242
|
+
manifest signs which statements the one control covered.
|
|
243
|
+
|
|
244
|
+
A `consent_to` never joins that line, under any configuration. It cannot be optional inside a
|
|
245
|
+
required sentence without silently becoming required; it cannot be separately withdrawable if
|
|
246
|
+
withdrawing it would mean withdrawing assent to the Terms; and it cannot be distinguishable from
|
|
247
|
+
other matters if it is one clause in a sentence about other matters. Only `agreement` and
|
|
248
|
+
`acknowledgment` are composable at all, so this is not a default somebody can configure their
|
|
249
|
+
way past — it is the shape of the code.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Reading current state
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
user.clickwraps.current_for?(:signup)
|
|
257
|
+
user.clickwraps.required_for?(:current_terms)
|
|
258
|
+
user.clickwraps.agreed_to?(:terms)
|
|
259
|
+
user.clickwraps.acknowledged?(:privacy_notice)
|
|
260
|
+
user.clickwraps.consented_to?(:product_updates)
|
|
261
|
+
user.clickwraps.declared?(:independent_contractor, subject: scheme)
|
|
262
|
+
user.clickwraps.attested?(:bank_accepted_transfer)
|
|
263
|
+
user.clickwraps.authorized?(:withdrawal, subject: withdrawal)
|
|
264
|
+
user.clickwraps.exempted_from?(:signup)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Every predicate has a structured form for when "no" needs an explanation:
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
result = Clickwrap.verify(:withdrawal_authorization, actor: user, subject: withdrawal)
|
|
271
|
+
|
|
272
|
+
result.success? # => false
|
|
273
|
+
result.error # => :declaration_expired
|
|
274
|
+
result.message # localized human explanation
|
|
275
|
+
result.event_id
|
|
276
|
+
result.details # stable machine-readable facts, no surprise personal data
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The convention is consistent: predicates answer booleans, `verify` returns a result, and bang
|
|
280
|
+
methods (`Clickwrap.require!`) raise a typed error carrying that same result. Applications never
|
|
281
|
+
need to parse an English message to make an authorization decision. The failure symbols are a
|
|
282
|
+
fixed vocabulary — `declaration_expired`, `consent_withdrawn`, `authorization_consumed`,
|
|
283
|
+
`superseded`, `wrong_subject`, `subject_fingerprint_mismatch`, `replay_rejected`, and the rest —
|
|
284
|
+
and like every other stable string in the gem, that list is added to, never renamed or
|
|
285
|
+
repurposed.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Sources
|
|
290
|
+
|
|
291
|
+
| Source | Class |
|
|
292
|
+
|---|---|
|
|
293
|
+
| [GDPR Article 6](https://eur-lex.europa.eu/eli/reg/2016/679/art_6/oj/eng) — lawful bases | Law |
|
|
294
|
+
| [GDPR Article 7](https://eur-lex.europa.eu/eli/reg/2016/679/art_7/oj/eng) — conditions for consent | Law |
|
|
295
|
+
| [GDPR Article 13](https://eur-lex.europa.eu/eli/reg/2016/679/art_13/oj/eng) — information to be provided | Law |
|
|
296
|
+
| [AEPD FAQ 02.48](https://www.aepd.es/preguntas-frecuentes/2-tus-obligaciones-como-responsable-del-tratamiento/6-el-deber-de-informacion/FAQ-0248-sobre-si-el-usuario-tiene-que-dar-consentimiento-a-clausula-de-privacidad) — a privacy-information checkbox is not blanket consent | Regulator guidance |
|
|
297
|
+
| [Directive 93/13/EEC](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=celex%3A31993L0013) — electronic form does not make an unfair term fair | Law |
|
|
298
|
+
| [15 U.S.C. § 7001](https://www.law.cornell.edu/uscode/text/15/7001), [§ 7003](https://www.law.cornell.edu/uscode/text/15/7003) — electronic validity preserved, substantive requirements and exclusions retained | Law |
|
|
299
|
+
| The six-kind taxonomy, the capability matrix, and the decision order above | Product-design inference |
|