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/guides/naming.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# The public naming contract
|
|
2
|
+
|
|
3
|
+
Every public name must say what it contains and where it came from. The test is literal: a
|
|
4
|
+
15-year-old reading the initializer, a policy, an error message, a task's output, or a receipt
|
|
5
|
+
should not need to know internal abbreviations to understand what the application does.
|
|
6
|
+
|
|
7
|
+
This is not style. Clickwrap's value is evidence that is still true and still legible years
|
|
8
|
+
after it was written, and the names are part of the evidence. A field called `ua` in a receipt
|
|
9
|
+
from 2026 is a puzzle in 2031. A method called `dispose!` is a method nobody can review. An
|
|
10
|
+
option called `full` is a decision nobody can audit.
|
|
11
|
+
|
|
12
|
+
The rules below are normative for the whole gem — public methods, configuration settings, DSL
|
|
13
|
+
keywords, error messages, generated comments, task output, and receipt fields — not only for
|
|
14
|
+
request evidence.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The eight rules
|
|
19
|
+
|
|
20
|
+
### 1. Prefer a complete verb plus noun
|
|
21
|
+
|
|
22
|
+
**Why:** a bare noun leaves the reader to guess the verb, and different readers guess
|
|
23
|
+
differently. "With ip" — with it how? Recorded? Filtered? Required?
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# Before
|
|
27
|
+
config.with_ip = true
|
|
28
|
+
policy.ip = :on
|
|
29
|
+
|
|
30
|
+
# After
|
|
31
|
+
config.record_ip_address_by_default = true
|
|
32
|
+
record_ip_address(because: "...", delete_after: 90.days)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Qualify the source
|
|
36
|
+
|
|
37
|
+
**Why:** "resolver" says something gets resolved. It does not say from what, which is exactly
|
|
38
|
+
the fact a reader needs in order to judge how much the value is worth. An address read from an
|
|
39
|
+
HTTP request and an address supplied by a client are different facts and must not share a name.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Before
|
|
43
|
+
config.ip_resolver = ->(x) { x.remote_ip }
|
|
44
|
+
|
|
45
|
+
# After
|
|
46
|
+
config.read_ip_address_from_http_request_with = ->(http_request) { http_request.remote_ip }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The same rule produces `read_browser_user_agent_from_http_request_with`, and it is why a
|
|
50
|
+
host-assigned reader is labeled `host_configured_reader` in the receipt rather than being
|
|
51
|
+
allowed to borrow the credibility of `rails_request_remote_ip`.
|
|
52
|
+
|
|
53
|
+
### 3. Positive booleans
|
|
54
|
+
|
|
55
|
+
**Why:** a negative boolean makes every reader do a double negative in their head, and
|
|
56
|
+
`disable_ip_capture = false` is a line that has been misread in every codebase that has ever
|
|
57
|
+
contained one.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# Before
|
|
61
|
+
config.disable_ip_capture = false
|
|
62
|
+
config.no_geolocation = true
|
|
63
|
+
|
|
64
|
+
# After
|
|
65
|
+
config.record_ip_address_by_default = true
|
|
66
|
+
config.record_ip_geolocation_country_by_default = false
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Destructive methods name exactly what they delete
|
|
70
|
+
|
|
71
|
+
**Why:** `dispose!` in a disposition report a year from now tells nobody which value went. Three
|
|
72
|
+
methods that each name one field mean the report, the audit event, and the log line all say
|
|
73
|
+
which category disappeared, rather than a euphemism covering several.
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# Before
|
|
77
|
+
Clickwrap.dispose!(receipt)
|
|
78
|
+
Clickwrap.purge_network_context!(receipt)
|
|
79
|
+
Clickwrap.delete_personal_data!(receipt)
|
|
80
|
+
|
|
81
|
+
# After
|
|
82
|
+
Clickwrap.delete_recorded_ip_address!(receipt, because: "Retention period ended")
|
|
83
|
+
Clickwrap.delete_recorded_browser_user_agent!(receipt, because: "Retention period ended")
|
|
84
|
+
Clickwrap.delete_recorded_ip_geolocation!(receipt, because: "Retention period ended")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The implementation may centralize disposal internally — it does. Public calls, task output,
|
|
88
|
+
audit events, and documentation keep the explicit field name.
|
|
89
|
+
|
|
90
|
+
Every destructive method also takes a plain-English `because:`. It is stored on the disposition
|
|
91
|
+
event and it is the only thing that will explain the deletion to somebody reading the record
|
|
92
|
+
years from now.
|
|
93
|
+
|
|
94
|
+
### 5. Distinguish configuration from fact
|
|
95
|
+
|
|
96
|
+
**Why:** "we are set up to record this" and "this was recorded" are different claims, and a
|
|
97
|
+
receipt that blurs them is worthless. The `_by_default` suffix marks a setting; the trailing
|
|
98
|
+
question mark marks an observation about one event.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
config.record_ip_address_by_default # configuration: what we intend to do
|
|
102
|
+
receipt.recorded_ip_address? # fact: what happened for this event
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The same distinction runs through the six request-evidence states. `not_configured` is a
|
|
106
|
+
configuration fact; `unavailable` is an observation; `deleted_after_retention` is a third thing
|
|
107
|
+
entirely. None of them is blank.
|
|
108
|
+
|
|
109
|
+
### 6. One option never secretly enables another category of data
|
|
110
|
+
|
|
111
|
+
**Why:** this is the failure mode the gem exists to prevent. An option that turns on a category
|
|
112
|
+
of personal data as a side effect makes the diff unreviewable and the upgrade dangerous — a
|
|
113
|
+
later release can widen what the profile covers, and nobody reading the initializer would know.
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
# Before
|
|
117
|
+
record_ip_geolocation(precision: :full)
|
|
118
|
+
record_request_context(level: :enhanced)
|
|
119
|
+
|
|
120
|
+
# After
|
|
121
|
+
record_ip_geolocation(
|
|
122
|
+
country: true,
|
|
123
|
+
region: true,
|
|
124
|
+
city: true,
|
|
125
|
+
postal_code: false,
|
|
126
|
+
latitude_and_longitude: true,
|
|
127
|
+
timezone: false,
|
|
128
|
+
continent: false,
|
|
129
|
+
metro_code: false,
|
|
130
|
+
accuracy_radius_in_kilometers: true,
|
|
131
|
+
because: "...",
|
|
132
|
+
retain_until: :security_evidence_retention_ends
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Nine fields, nine visible decisions, each with its own line in the privacy inventory.
|
|
137
|
+
`latitude_and_longitude` is one coupled choice on purpose: half a coordinate is not a result.
|
|
138
|
+
|
|
139
|
+
The installer follows the same rule. Interactive prompts ask about one category at a time, and
|
|
140
|
+
non-interactive installs use individual flags such as
|
|
141
|
+
`--record-ip-addresses-by-default`. The sole recipe,
|
|
142
|
+
`--request-evidence-recipe=privacy-minimized`, writes every collection setting explicitly as
|
|
143
|
+
`false`; it can never enable a data category. There is no `evidence-rich`, `full`, or other named
|
|
144
|
+
recipe whose expansion could collect more in a later release.
|
|
145
|
+
|
|
146
|
+
### 7. No unexplained acronyms or abbreviations
|
|
147
|
+
|
|
148
|
+
**Why:** an abbreviation is a shared secret between the author and whoever was in the room. A
|
|
149
|
+
receipt is read by people who were not.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Before
|
|
153
|
+
receipt.ua
|
|
154
|
+
receipt.geo
|
|
155
|
+
event.addr
|
|
156
|
+
capture!(:signup, ctx: request)
|
|
157
|
+
|
|
158
|
+
# After
|
|
159
|
+
receipt.browser_user_agent
|
|
160
|
+
receipt.ip_geolocation_country_code
|
|
161
|
+
receipt.ip_address
|
|
162
|
+
Clickwrap.capture!(:signup, http_request: request)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Two abbreviations are permitted because they are the standards' own names and expanding them
|
|
166
|
+
would be less clear, not more: `ip` inside `ip_address` and `ip_geolocation`, and `http` inside
|
|
167
|
+
`http_request`.
|
|
168
|
+
|
|
169
|
+
### 8. Every example must make sense read aloud
|
|
170
|
+
|
|
171
|
+
**Why:** it is the cheapest review anyone can run, and it catches almost everything the other
|
|
172
|
+
seven rules are trying to prevent.
|
|
173
|
+
|
|
174
|
+
Read this aloud:
|
|
175
|
+
|
|
176
|
+
> Clickwrap capture and, withdrawal authorization, actor current user, subject withdrawal, http
|
|
177
|
+
> request request, submission clickwrap submission.
|
|
178
|
+
|
|
179
|
+
It is a sentence. Now read the alternative:
|
|
180
|
+
|
|
181
|
+
> Clickwrap capture, with ip, ctx request, full.
|
|
182
|
+
|
|
183
|
+
If an example does not survive being read aloud by a developer who has never seen the gem, the
|
|
184
|
+
name is wrong. Fix the name, not the example.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Required vocabulary
|
|
189
|
+
|
|
190
|
+
Use the left column. The right column is what has been rejected and why.
|
|
191
|
+
|
|
192
|
+
| Use | Not | Because |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `ip_address` | `ip`, `remote_address`, `network_address`, `addr` | Abbreviations, and "network address" hides which address |
|
|
195
|
+
| `ip_geolocation` | `geo`, `location`, `coordinates` | "Location" reads as where the person is. It is an estimate about an address |
|
|
196
|
+
| `ip_geolocation_latitude_and_longitude` | `precise_location`, `fine_location` | "Precise" is the opposite of true here |
|
|
197
|
+
| `ip_geolocation_accuracy_radius_in_kilometers` | `accuracy` | A number with no unit and no meaning |
|
|
198
|
+
| `ip_geolocation_provider_name`, `ip_geolocation_provider_source`, `ip_geolocation_database_version` | `provider`, `source` | In a receipt, "source" could mean four things |
|
|
199
|
+
| `browser_user_agent` | `ua`, `client_info`, `browser` | The raw header is a specific thing; "browser" suggests a parsed result |
|
|
200
|
+
| `http_request` | `context`, `ctx`, `env` | "Context" is where unreviewed data goes to hide |
|
|
201
|
+
| `capture_channel` | `flow`, `source` | Unqualified nouns again |
|
|
202
|
+
| `authentication_method` | `assurance` | Assurance supplied by whom, measured how? |
|
|
203
|
+
| `recorded_at_by_server` | `signed_at`, `timestamped_at` | Nothing signed it. It is the application server's own clock, and the name has to keep saying so |
|
|
204
|
+
| `estimated`, `client_supplied`, `server_observed`, `provider_reported` | dropping the qualifier | These four words are the difference between a fact and an overclaim |
|
|
205
|
+
|
|
206
|
+
## Booleans read as questions
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
receipt.recorded_ip_address?
|
|
210
|
+
receipt.recorded_browser_user_agent?
|
|
211
|
+
receipt.recorded_ip_geolocation_country?
|
|
212
|
+
receipt.recorded_ip_geolocation_city?
|
|
213
|
+
receipt.recorded_ip_geolocation_latitude_and_longitude?
|
|
214
|
+
receipt.ip_geolocation_was_estimated?
|
|
215
|
+
receipt.ip_geolocation_source_was_verified_by_host?
|
|
216
|
+
receipt.ip_address_was_deleted?
|
|
217
|
+
receipt.browser_user_agent_was_deleted?
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The `was_` prefix on the last four is doing work: `ip_geolocation_was_estimated?` is a statement
|
|
221
|
+
about the value that was stored, not about the resolver's current settings, and
|
|
222
|
+
`ip_address_was_deleted?` is a fact about this record rather than about policy.
|
|
223
|
+
|
|
224
|
+
The same grammar runs through the actor proxy, which is where most application code meets the
|
|
225
|
+
gem:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
user.clickwraps.agreed_to?(:terms)
|
|
229
|
+
user.clickwraps.acknowledged?(:privacy_notice)
|
|
230
|
+
user.clickwraps.consented_to?(:product_updates)
|
|
231
|
+
user.clickwraps.declared?(:independent_contractor, subject: scheme)
|
|
232
|
+
user.clickwraps.attested?(:bank_accepted_transfer)
|
|
233
|
+
user.clickwraps.authorized?(:withdrawal, subject: withdrawal)
|
|
234
|
+
user.clickwraps.exempted_from?(:signup)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Six kinds, six predicates. There is no generic `accepted?`, because collapsing them would erase
|
|
238
|
+
the distinction the six kinds exist to preserve — and `exempted_from?` is separate precisely so
|
|
239
|
+
an exemption can never answer a human-action question.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Prohibited option names
|
|
244
|
+
|
|
245
|
+
These may not appear as public options, keywords, settings, or command-line flags. Each one is
|
|
246
|
+
banned for a specific reason, not for taste.
|
|
247
|
+
|
|
248
|
+
| Never | Why |
|
|
249
|
+
|---|---|
|
|
250
|
+
| `:network`, `:full`, `:enhanced`, `:forensic`, `:maximum` | They hide what will be collected behind a word that sounds like a quality level |
|
|
251
|
+
| `record_location` | A developer could reasonably read it as GPS or physical location. It is neither |
|
|
252
|
+
| `request_evidence: :network`, `track_everything`, `record_everything` | Category switches. See rule 6 |
|
|
253
|
+
| `maximum_evidence`, `full_evidence`, `legal_proof: true` | They imply a verdict the gem cannot reach, and they enable data as a side effect |
|
|
254
|
+
| An opaque privacy-profile switch keyed to a regulation | No runtime flag can make a legal determination on anyone's behalf, and the name would be the least accurate string in the codebase |
|
|
255
|
+
| `include_sensitive_context: true` | One flag turning on three categories of personal data makes an operator's intent unreviewable. Use `include_ip_address:`, `include_browser_user_agent:`, `include_ip_geolocation:` |
|
|
256
|
+
| `dispose!`, `purge!`, `cleanup!` as public API | See rule 4 |
|
|
257
|
+
|
|
258
|
+
There is also no `--gdpr-*`, `--record-network-context`, `--record-everything`, or
|
|
259
|
+
`--full-evidence` generator flag. Non-interactive installer options are as explicit as the
|
|
260
|
+
settings they write:
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
--record-ip-addresses-by-default
|
|
264
|
+
--record-ip-geolocation-cities-by-default
|
|
265
|
+
--record-ip-geolocation-latitude-and-longitude-by-default
|
|
266
|
+
--delete-recorded-ip-addresses-after-days=90
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## The same standard for the rest of the initializer
|
|
272
|
+
|
|
273
|
+
The rule is not a request-evidence rule. It produced the shape of the whole configuration
|
|
274
|
+
object.
|
|
275
|
+
|
|
276
|
+
| Shipped name | Rejected | Why |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| `actor_class_name` | `actor_class` | It holds a string, constantized lazily so the initializer works before the model loads. The name says which |
|
|
279
|
+
| `current_actor_method_name` | `current_actor` | It is a method name, not an actor |
|
|
280
|
+
| `find_current_tenant_with` | `tenant_resolver` | Verb plus source. It is a callable that finds something |
|
|
281
|
+
| `store_document_contents_in` | `document_store` | Says what goes where |
|
|
282
|
+
| `digest_canonical_receipts_with` | `integrity` | "Integrity" is a category, not a decision. This one names the algorithm's job |
|
|
283
|
+
| `chain_event_history_with`, `anchor_event_history_with`, `timestamp_receipts_with` | one `integrity_level` setting | Three different mechanisms making three different claims. One setting would let a reader infer the strongest from the presence of the weakest |
|
|
284
|
+
| `after_event_is_committed` | `after_commit` | Says which commit, and reads as a sentence |
|
|
285
|
+
| `authorize_unredacted_request_evidence_access_with` | `access_control` | Long, and correct. It names exactly which access it authorizes |
|
|
286
|
+
| `deliberately_store_request_evidence_unencrypted!(because:)` | `encryption: false` | Turning encryption off should be a sentence a reviewer can find in a diff, with the host's own reason attached — not a `false` |
|
|
287
|
+
|
|
288
|
+
The last row is the pattern worth copying. When an option has a consequence somebody should
|
|
289
|
+
have to think about, make the name carry the thinking.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Reviewing a new name
|
|
294
|
+
|
|
295
|
+
Before you add a public method, option, setting, or receipt field:
|
|
296
|
+
|
|
297
|
+
- [ ] Read the example aloud. Does it parse as English?
|
|
298
|
+
- [ ] Does the name say what it contains **and** where it came from?
|
|
299
|
+
- [ ] Is the boolean positive?
|
|
300
|
+
- [ ] If it deletes something, does the name say which thing?
|
|
301
|
+
- [ ] Does it distinguish "we are configured to" from "this happened"?
|
|
302
|
+
- [ ] Could enabling it turn on a category of personal data the reader did not name?
|
|
303
|
+
- [ ] Are there abbreviations that are not `ip` or `http`?
|
|
304
|
+
- [ ] If it goes into a receipt: will it still be legible in 2031, to somebody who has never
|
|
305
|
+
seen this codebase?
|
|
306
|
+
- [ ] If it goes into a receipt: is it in `Clickwrap::Vocabulary`? Stable strings live there,
|
|
307
|
+
frozen, in one place, and they are **added to, never renamed or repurposed** — a value
|
|
308
|
+
that changed meaning underneath an old receipt would make it say something it never said.
|
|
309
|
+
|
|
310
|
+
A name that fails any of these is cheaper to change now than after the first release, because
|
|
311
|
+
after the first release it is in receipts.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Sources
|
|
316
|
+
|
|
317
|
+
| Source | Class |
|
|
318
|
+
|---|---|
|
|
319
|
+
| `docs/strategy/02-request-evidence.md`, "Public naming rules" — the normative source for this guide | Internal normative design document |
|
|
320
|
+
| Every rule, rejection, and rationale above | Product-design inference |
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Binding an organization through a human actor
|
|
2
|
+
|
|
3
|
+
Clickwrap keeps the person who acted and the party they represented as two
|
|
4
|
+
different facts:
|
|
5
|
+
|
|
6
|
+
| Concept | Typical `organizations` record | What it means |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| actor | `current_user` | The human account that performed the clickwrap action |
|
|
9
|
+
| represented party (`acting_for`) | `current_organization` | The organization the action is intended to bind |
|
|
10
|
+
| tenant | usually `current_organization` | The application data boundary in which the action happened |
|
|
11
|
+
| subject | optional domain record | The exact order, withdrawal, contract, or other object the statement covers |
|
|
12
|
+
|
|
13
|
+
A `User` therefore does not disappear behind an organization. The event and
|
|
14
|
+
receipt keep both references, plus the membership evidence used to authorize
|
|
15
|
+
the represented action.
|
|
16
|
+
|
|
17
|
+
## One policy line
|
|
18
|
+
|
|
19
|
+
Choose the organization role your application and counsel have reviewed:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
Clickwrap.policy :organization_terms do
|
|
23
|
+
agree_to :organization_terms
|
|
24
|
+
|
|
25
|
+
permit_acting_for_organization when_actor_is_at_least: :admin
|
|
26
|
+
|
|
27
|
+
retain_with :ordinary_agreement_evidence
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or name a purpose-specific permission from the `organizations` gem instead of
|
|
32
|
+
coupling the decision to a broad role:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Clickwrap.policy :data_processing_terms do
|
|
36
|
+
agree_to :data_processing_terms
|
|
37
|
+
|
|
38
|
+
permit_acting_for_organization \
|
|
39
|
+
when_actor_has_permission: :accept_data_processing_terms
|
|
40
|
+
|
|
41
|
+
retain_with :ordinary_agreement_evidence
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If both options are present, both must pass. A policy with neither is rejected
|
|
46
|
+
at boot: organization membership by itself is not treated as legal authority.
|
|
47
|
+
|
|
48
|
+
The permission-oriented form is usually the more durable design. It lets an
|
|
49
|
+
application grant exactly “may accept these terms for this organization”
|
|
50
|
+
without silently equating that power with every other thing an `admin` can do.
|
|
51
|
+
|
|
52
|
+
## Present and capture the same represented party
|
|
53
|
+
|
|
54
|
+
Make organizational capacity visible in both the interface copy and the Ruby
|
|
55
|
+
call:
|
|
56
|
+
|
|
57
|
+
```erb
|
|
58
|
+
<%= form_with model: @agreement do |form| %>
|
|
59
|
+
<p>
|
|
60
|
+
You are accepting these terms for
|
|
61
|
+
<strong><%= current_organization.name %></strong>.
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<%= form.clickwrap :organization_terms,
|
|
65
|
+
acting_for: current_organization,
|
|
66
|
+
submit: "Accept for #{current_organization.name}" %>
|
|
67
|
+
<% end %>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then pass the server-owned organization again at submit:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
def create
|
|
74
|
+
organization = current_organization
|
|
75
|
+
|
|
76
|
+
capture_clickwrap_and!(
|
|
77
|
+
:organization_terms,
|
|
78
|
+
acting_for: organization
|
|
79
|
+
) do |pending_receipt|
|
|
80
|
+
organization.update!(
|
|
81
|
+
terms_accepted_with_clickwrap_event_id: pending_receipt.event_id
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
redirect_to organization_settings_path
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Do not permit an organization ID from the form and turn it into
|
|
90
|
+
`acting_for:`. Resolve the organization from the authenticated server-side
|
|
91
|
+
context. The signed presentation is also bound to that exact represented-party
|
|
92
|
+
reference, so it cannot be moved to another organization between render and
|
|
93
|
+
submit.
|
|
94
|
+
|
|
95
|
+
If the application uses the organization as its Clickwrap tenant too, configure
|
|
96
|
+
the resolver once — **and declare tenant semantics on every policy**, because
|
|
97
|
+
the resolver alone reproduces a real production bug: an organization member's
|
|
98
|
+
session resolves an ambient organization on every request, so a *personal*
|
|
99
|
+
policy (a contractor declaration, a payout gate) would silently bind — or refuse —
|
|
100
|
+
under whatever organization happened to be current:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
Clickwrap.configure do |config|
|
|
104
|
+
config.find_current_tenant_with = ->(controller) {
|
|
105
|
+
controller.current_organization
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
Clickwrap.policy :contractor_declaration do
|
|
110
|
+
tenant_is :not_applicable # personal evidence: joining an org changes nothing
|
|
111
|
+
|
|
112
|
+
declare :independent_contractor,
|
|
113
|
+
document: :terms,
|
|
114
|
+
statement: "I provide these services as an independent contractor, not as an employee."
|
|
115
|
+
|
|
116
|
+
retain_with :ordinary_agreement_evidence
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
Clickwrap.policy :organization_terms do
|
|
120
|
+
tenant_is :required # org-scoped evidence: the org is passed deliberately
|
|
121
|
+
|
|
122
|
+
agree_to :terms
|
|
123
|
+
permit_acting_for_organization when_actor_is_at_least: :admin
|
|
124
|
+
|
|
125
|
+
retain_with :ordinary_agreement_evidence
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`tenant_is :not_applicable` policies always record a nil tenant regardless of
|
|
130
|
+
the ambient organization; `:required` policies refuse to present or capture
|
|
131
|
+
without one; `:optional` (the default) passes the resolved value through.
|
|
132
|
+
Presentation, capture, verification, withdrawal, and import all translate the
|
|
133
|
+
ambient value through the same declaration. `acting_for:` stays explicit
|
|
134
|
+
because “this happened inside Acme” and “this person intended to bind Acme”
|
|
135
|
+
are not interchangeable claims.
|
|
136
|
+
|
|
137
|
+
## What is checked at presentation and submit
|
|
138
|
+
|
|
139
|
+
The built-in adapter has no hard runtime dependency on `organizations`; it is
|
|
140
|
+
used only by a policy that calls `permit_acting_for_organization`. For a
|
|
141
|
+
persisted organization it:
|
|
142
|
+
|
|
143
|
+
1. requires a persisted `Organizations::Organization` (host subclasses are
|
|
144
|
+
accepted);
|
|
145
|
+
2. requires the actor to have a current membership in that exact organization
|
|
146
|
+
before rendering the form;
|
|
147
|
+
3. signs the presentation-time membership reference, role, source, criterion,
|
|
148
|
+
and verification time into the manifest;
|
|
149
|
+
4. locks and rereads the membership inside the capture transaction;
|
|
150
|
+
5. checks the configured minimum role and/or permission at both moments; and
|
|
151
|
+
6. records both snapshots plus the available authentication method.
|
|
152
|
+
|
|
153
|
+
That means an admin removed or demoted after the page was rendered is denied at
|
|
154
|
+
submit. A role change that remains authorized is not flattened: the receipt
|
|
155
|
+
keeps the role at presentation and the role at capture. A token rendered for
|
|
156
|
+
one organization cannot be submitted for another, and an organizational
|
|
157
|
+
acceptance never satisfies a personal-capacity query:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
user.clickwraps.current_for?(
|
|
161
|
+
:organization_terms,
|
|
162
|
+
tenant: organization,
|
|
163
|
+
acting_for: organization
|
|
164
|
+
) # => true
|
|
165
|
+
|
|
166
|
+
user.clickwraps.current_for?(
|
|
167
|
+
:organization_terms,
|
|
168
|
+
tenant: organization
|
|
169
|
+
) # => false
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Create the organization and its evidence together
|
|
173
|
+
|
|
174
|
+
A person may reach the form before the organization has a row or a membership.
|
|
175
|
+
That is not the ordinary `acting_for:` case: Clickwrap cannot truthfully say it
|
|
176
|
+
verified a membership that does not exist. Opt into the prospective flow in the
|
|
177
|
+
policy, and include an explicit declaration for the real-world authority or
|
|
178
|
+
content-rights claim the application needs:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
Clickwrap.policy :organization_creation do
|
|
182
|
+
declare :authority_and_content_rights,
|
|
183
|
+
statement: "I am authorized to create and act for this organization and may use the content I submit.",
|
|
184
|
+
document: nil,
|
|
185
|
+
protected_outcome_version: "created-organization-v1",
|
|
186
|
+
record_protected_outcome_with: ->(organization) {
|
|
187
|
+
Clickwrap.protected_outcome(
|
|
188
|
+
action: :created,
|
|
189
|
+
record: organization,
|
|
190
|
+
facts: {
|
|
191
|
+
name: organization.name,
|
|
192
|
+
logo_checksum: organization.logo.blob.checksum
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
permit_acting_for_organization(
|
|
198
|
+
when_actor_is_at_least: :owner,
|
|
199
|
+
including_when_this_action_creates_the_organization: true
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
retain_with :ordinary_agreement_evidence
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Pass the same new model as `acting_for:`. The form helper creates and signs a
|
|
207
|
+
server-owned browser-flow identifier; there is no organization id, role, policy
|
|
208
|
+
option, or evidence field for the browser to choose:
|
|
209
|
+
|
|
210
|
+
```erb
|
|
211
|
+
<%= form_with model: @organization do |form| %>
|
|
212
|
+
<%# name, logo, and other organization fields %>
|
|
213
|
+
|
|
214
|
+
<%= form.clickwrap :organization_creation,
|
|
215
|
+
acting_for: @organization,
|
|
216
|
+
submit: "Create organization" %>
|
|
217
|
+
<% end %>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
At submit, return the persisted organization after creating its owner
|
|
221
|
+
membership in the protected block. The result may come from an ordinary
|
|
222
|
+
creation service; it must have the same class the presentation bound:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
def create
|
|
226
|
+
@organization = Organizations::Organization.new(organization_params)
|
|
227
|
+
|
|
228
|
+
receipt = create_represented_party_with_clickwrap(
|
|
229
|
+
:organization_creation,
|
|
230
|
+
represented_party: @organization
|
|
231
|
+
) do |pending_receipt|
|
|
232
|
+
@organization.save!
|
|
233
|
+
@organization.add_member!(current_user, role: :owner)
|
|
234
|
+
@organization.update!(creation_clickwrap_event_id: pending_receipt.event_id)
|
|
235
|
+
@organization
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
redirect_to organization_path(receipt.event.represented_party)
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The manifest labels presentation-time authority `not_yet_verifiable`. After the
|
|
243
|
+
block returns the persisted record, the configured adapter verifies the new
|
|
244
|
+
owner membership inside the same transaction, and the event is rebound to the final
|
|
245
|
+
stable organization reference before its digest and projections are written.
|
|
246
|
+
An evidence-write failure, model-validation failure, missing/insufficient
|
|
247
|
+
membership, protected-outcome failure, or outer transaction rollback leaves no
|
|
248
|
+
created organization or Clickwrap event. An identical nonce retry returns the
|
|
249
|
+
original receipt without running the creation block twice.
|
|
250
|
+
|
|
251
|
+
That post-creation owner check proves only the application state the block just
|
|
252
|
+
created. It does not prove that the person already had real-world authorization
|
|
253
|
+
to use a legal name or logo or to bind an external company. Record that claim as
|
|
254
|
+
an explicit `declare` statement, choose its wording with counsel, and keep the
|
|
255
|
+
protected outcome specific enough to identify the organization and submitted
|
|
256
|
+
assets it covered.
|
|
257
|
+
|
|
258
|
+
API clients use the same primitive with a server-owned flow id:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
Clickwrap.create_represented_party!(
|
|
262
|
+
:organization_creation,
|
|
263
|
+
actor: current_user,
|
|
264
|
+
represented_party: organization,
|
|
265
|
+
represented_party_creation_flow_id: server_session_flow_id,
|
|
266
|
+
http_request: request,
|
|
267
|
+
submission: Clickwrap.submission_from(params)
|
|
268
|
+
) do |pending_receipt|
|
|
269
|
+
organization.save!
|
|
270
|
+
organization.add_member!(current_user, role: :owner)
|
|
271
|
+
organization.update!(creation_clickwrap_event_id: pending_receipt.event_id)
|
|
272
|
+
organization
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Pass that same `represented_party_creation_flow_id:` to `Clickwrap.present`.
|
|
277
|
+
Do not derive it from form fields or accept it as the authority decision; it is
|
|
278
|
+
opaque server session state used only to bind one prospective browser flow.
|
|
279
|
+
|
|
280
|
+
## The legal boundary
|
|
281
|
+
|
|
282
|
+
The `organizations` role or permission is an application authorization fact.
|
|
283
|
+
Clickwrap records that fact and its provenance; it cannot decide whether an
|
|
284
|
+
`admin`, officer, employee, guardian, or agent has legal capacity to bind a
|
|
285
|
+
party for a particular agreement in a particular jurisdiction. Spain's Civil
|
|
286
|
+
Code Article 1259, for example, makes authorization or legal representation a
|
|
287
|
+
substantive issue and addresses later ratification; it does not say that a SaaS
|
|
288
|
+
role named `admin` or `owner` supplies that authorization ([official current
|
|
289
|
+
consolidated text](https://www.boe.es/eli/es/rd/1889/07/24/%281%29/con),
|
|
290
|
+
[official BOE document view with Article 1259](https://www.boe.es/buscar/doc.php?id=BOE-A-1889-4763&lang=es)).
|
|
291
|
+
EU eIDAS Article 25 also distinguishes the evidential treatment of an
|
|
292
|
+
electronic signature from the specific handwritten-signature equivalence of a
|
|
293
|
+
qualified electronic signature ([official consolidated EUR-Lex
|
|
294
|
+
text](https://eur-lex.europa.eu/eli/reg/2014/910/en/cons)).
|
|
295
|
+
|
|
296
|
+
Those are jurisdiction-specific legal sources, not a universal answer. Choose
|
|
297
|
+
the criterion and statement with counsel, use interface copy that makes the
|
|
298
|
+
represented capacity conspicuous, and keep any organization identity fields
|
|
299
|
+
your evidentiary policy requires. The built-in adapter records stable
|
|
300
|
+
database/GlobalID references; it does not claim that an organization display
|
|
301
|
+
name is a verified legal name, that a membership role is statutory authority,
|
|
302
|
+
or that an ordinary click is a qualified electronic signature.
|
|
303
|
+
|
|
304
|
+
For a different authority system, keep the same actor/represented-party model
|
|
305
|
+
and register a named server-side adapter with
|
|
306
|
+
`config.register_represented_party_authority`.
|
|
307
|
+
|
|
308
|
+
## Exact implementation sources
|
|
309
|
+
|
|
310
|
+
These are source-code observations, not legal authorities:
|
|
311
|
+
|
|
312
|
+
- The `organizations` gem's public organization and membership APIs are
|
|
313
|
+
documented at
|
|
314
|
+
https://github.com/rameerez/organizations/blob/fd0eace263c54cd3c8ab98a9505190df6856c6d6/README.md#the-complete-api
|
|
315
|
+
- Its organization-to-membership association is implemented at
|
|
316
|
+
https://github.com/rameerez/organizations/blob/fd0eace263c54cd3c8ab98a9505190df6856c6d6/lib/organizations/models/organization.rb#L37-L41
|
|
317
|
+
- Its membership role and permission checks are implemented at
|
|
318
|
+
https://github.com/rameerez/organizations/blob/fd0eace263c54cd3c8ab98a9505190df6856c6d6/lib/organizations/models/membership.rb#L106-L160
|
|
319
|
+
- Its default role hierarchy and permission evaluation are implemented at
|
|
320
|
+
https://github.com/rameerez/organizations/blob/fd0eace263c54cd3c8ab98a9505190df6856c6d6/lib/organizations/roles.rb#L12-L146
|