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
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# Request evidence: the data dictionary
|
|
2
|
+
|
|
3
|
+
Clickwrap always records the event ID, the server-recorded time, the capture channel, the
|
|
4
|
+
configured actor and authentication source, the policy and application version, and the HTTP
|
|
5
|
+
request ID when one is available. None of that is derived from the person's network or browser.
|
|
6
|
+
|
|
7
|
+
It records **nothing** about the request itself — no IP address, no browser user-agent, no
|
|
8
|
+
IP-geolocation field — unless the initializer or a policy names that exact field. There is no
|
|
9
|
+
category switch, no profile, and no option that turns one of these on as a side effect of
|
|
10
|
+
turning on something else.
|
|
11
|
+
|
|
12
|
+
That default is evidence design, not squeamishness. Three things follow from it:
|
|
13
|
+
|
|
14
|
+
- An IP address and other online identifiers can be personal data. The CJEU addressed dynamic
|
|
15
|
+
IP addresses in [Breyer, Case C-582/14](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A62014CJ0582)
|
|
16
|
+
*(case)*. Keeping the value on your own infrastructure does not remove purpose limitation,
|
|
17
|
+
data minimization, storage limitation, transparency, security, or protection-by-default
|
|
18
|
+
duties ([GDPR Article 5](https://eur-lex.europa.eu/eli/reg/2016/679/art_5/oj/eng),
|
|
19
|
+
[Article 13](https://eur-lex.europa.eu/eli/reg/2016/679/art_13/oj/eng),
|
|
20
|
+
[Article 25](https://eur-lex.europa.eu/eli/reg/2016/679/art_25/oj/eng),
|
|
21
|
+
[Article 32](https://eur-lex.europa.eu/eli/reg/2016/679/art_32/oj/eng)) *(law)*.
|
|
22
|
+
- Request evidence corroborates; it does not repair. In
|
|
23
|
+
[Berman v. Freedom Financial Network](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/05/20-16900.pdf)
|
|
24
|
+
and [Tejon v. Zeus Networks](https://media.ca11.uscourts.gov/opinions/pub/files/202411114.pdf)
|
|
25
|
+
*(cases)* weak notice and ambiguous action defeated the asserted agreement; more IP metadata
|
|
26
|
+
would not have changed either outcome. [Toth v. Everly Well](https://www.ca1.uscourts.gov/sites/ca1/files/opnfiles/23-1727P-01A.pdf)
|
|
27
|
+
*(case)* turned on a clear checkbox, linked terms, placement, and affirmative action.
|
|
28
|
+
- Active browser and device fingerprinting is out of scope for the base gem entirely. EDPB
|
|
29
|
+
Guidelines 2/2023 explain that fingerprinting and collection of protocol/device information
|
|
30
|
+
can fall within the technical scope of ePrivacy Directive Article 5(3), with exemptions
|
|
31
|
+
requiring case-specific analysis
|
|
32
|
+
([official PDF](https://www.edpb.europa.eu/system/files/documents/2024-10/edpb_guidelines_202302_technical_scope_art_53_eprivacydirective_v2_en_0.pdf))
|
|
33
|
+
*(regulator guidance)*. Clickwrap emits no canvas, audio, font, hardware, GPS, or
|
|
34
|
+
high-entropy Client Hint probe anywhere.
|
|
35
|
+
|
|
36
|
+
The ordering below is a product-design inference drawn from those cases and from the principle
|
|
37
|
+
that evidence is assessed with all its circumstances and the accuracy of the producing process
|
|
38
|
+
([Federal Rule of Evidence 901](https://www.law.cornell.edu/rules/fre/rule_901)) *(law)*. It is
|
|
39
|
+
not a statutory ranking:
|
|
40
|
+
|
|
41
|
+
1. exact content and conspicuous presentation;
|
|
42
|
+
2. explicit action by an authenticated actor;
|
|
43
|
+
3. atomic binding to the protected transaction;
|
|
44
|
+
4. integrity, reproducibility, and lifecycle; then
|
|
45
|
+
5. IP address, browser user-agent, and estimated IP geolocation.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## How to read the dictionary
|
|
50
|
+
|
|
51
|
+
Four tables follow, one per group. Columns are the same in each:
|
|
52
|
+
|
|
53
|
+
- **Field** — the exact name in the schema, the receipt, and the privacy inventory.
|
|
54
|
+
- **What it is** — one sentence.
|
|
55
|
+
- **Where it comes from** — the reader, resolver, or computation that produced it.
|
|
56
|
+
- **Does not establish** — the overclaim this field invites, said out loud.
|
|
57
|
+
- **Encrypted** — whether the value itself is encrypted at rest for this recorded annex.
|
|
58
|
+
- **Who can read it** — who gets the value rather than a state word.
|
|
59
|
+
- **In a default export** — whether `to_canonical_json` / `Clickwrap.export_receipt` with no
|
|
60
|
+
`include_*` flags contains it.
|
|
61
|
+
- **Deletion trigger** — what makes it go away.
|
|
62
|
+
- **After deletion the receipt says** — the state a reader sees afterwards.
|
|
63
|
+
|
|
64
|
+
Two rules apply to every row in every table.
|
|
65
|
+
|
|
66
|
+
**Reading a raw value is authorized and recorded.** `Clickwrap.export_receipt` will not return
|
|
67
|
+
`ip_address`, `browser_user_agent`, or IP-geolocation values unless you name that category
|
|
68
|
+
explicitly, supply a non-empty `because:`, and
|
|
69
|
+
`config.authorize_unredacted_request_evidence_access_with` returns true for the request. The
|
|
70
|
+
default callback returns `false` for everyone. Every authorized read appends a `ReceiptAccess`
|
|
71
|
+
row recording who asked, why, and which categories were included.
|
|
72
|
+
|
|
73
|
+
**Absence is never blank.** Every field reports one of six states —
|
|
74
|
+
`not_configured`, `unavailable`, `recorded`, `redacted_for_this_viewer`,
|
|
75
|
+
`deleted_after_retention`, `held` — so "we chose not to collect this", "collection failed",
|
|
76
|
+
"you may not see this", and "we deleted it on schedule" never look alike.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## IP address
|
|
81
|
+
|
|
82
|
+
The raw address and everything recorded alongside it.
|
|
83
|
+
|
|
84
|
+
| Field | What it is | Where it comes from | Does not establish | Encrypted | Who can read it | In a default export | Deletion trigger | After deletion the receipt says |
|
|
85
|
+
|---|---|---|---|---|---|---|---|---|
|
|
86
|
+
| `ip_address` | The single address observed for this HTTP request | `config.read_ip_address_from_http_request_with`, whose default is `->(http_request) { http_request.remote_ip }` | Who was at the keyboard. Addresses are shared, reassigned, NATed, proxied, and routed through VPNs | Yes — `encrypt_recorded_ip_addresses` is `true` by default and applies Active Record encryption to the `ip_address_ciphertext` column | Only a viewer for whom `authorize_unredacted_request_evidence_access_with` returns true, with a `because:` | No | Whichever the policy set: `delete_after:` writes `ip_address_delete_after` at capture, `retain_until:` writes `ip_address_retain_until_rule`. Otherwise `config.delete_recorded_ip_addresses_after`. Applied by `clickwrap:retention:apply` or `Clickwrap.delete_recorded_ip_address!` | `{"state": "deleted_after_retention", "deleted_at": "..."}` |
|
|
87
|
+
| `ip_address_reader_name` | Which reader produced the address: `rails_request_remote_ip` or `host_configured_reader` | Compared against a fresh `Configuration`'s default lambda; any host-assigned reader is labeled `host_configured_reader` even if its body is identical | That the address is correct. It says whose logic to blame or credit | No | Same as the address | No | Kept — it explains where the deleted value came from | Unchanged |
|
|
88
|
+
| `trusted_proxy_configuration_digest` | A digest of the host's reviewed trusted-proxy configuration, if the host set one | `config.trusted_proxy_configuration_digest` (default `nil`) | That the configuration was correct. It records which configuration was in force | No | Same as the address | No | Kept | Unchanged |
|
|
89
|
+
| `ip_address_recorded_at` | When the address was written | Server clock at capture | Anything about the person | No | Same as the address | No | Kept | Unchanged |
|
|
90
|
+
| `ip_address_delete_after` | The date this value became due for deletion | Written at capture from the policy or configuration rule | — | No | Same as the address | No | Kept | Unchanged |
|
|
91
|
+
| `ip_address_retain_until_rule` | The name of the host retention calculation governing this value, when a duration could not express it | Written at capture from `retain_until:` | That the rule has resolved. It may not have | No | Same as the address | No | Kept | Unchanged |
|
|
92
|
+
| `ip_address_deleted_at` | When the value was deleted | Set by the disposition | — | No | Same as the address | No | — | This is what makes the state `deleted_after_retention` |
|
|
93
|
+
| `ip_address_unavailable_reason` | Why no address was stored: `no_http_request`, `capture_channel_carries_no_http_request`, `no_ip_address_on_http_request`, or `ip_address_reader_returned_a_forwarded_chain` | Set instead of a value when the reader produced nothing usable | — | No | Anyone who can read the receipt | Yes — it appears in the fragment when the state is `unavailable` | — | — |
|
|
94
|
+
|
|
95
|
+
`ip_address_reader_returned_a_forwarded_chain` deserves its own sentence. If the configured
|
|
96
|
+
reader hands back a value containing a comma, Clickwrap refuses to store it. Everything after
|
|
97
|
+
the first trusted hop in a forwarding chain is client-supplied and can be anything at all;
|
|
98
|
+
filing a whole chain under "the address the server observed" would present attacker-controlled
|
|
99
|
+
input as an observation.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Browser user-agent
|
|
104
|
+
|
|
105
|
+
| Field | What it is | Where it comes from | Does not establish | Encrypted | Who can read it | In a default export | Deletion trigger | After deletion the receipt says |
|
|
106
|
+
|---|---|---|---|---|---|---|---|---|
|
|
107
|
+
| `browser_user_agent` | The raw `User-Agent` header string the client sent | `config.read_browser_user_agent_from_http_request_with`, default `->(http_request) { http_request.user_agent }` | Device identity, a unique fingerprint, or what was actually rendered on screen. The header is client-supplied and can say anything | Yes — `encrypt_recorded_browser_user_agents` is `true` by default and applies Active Record encryption to `browser_user_agent_ciphertext` | Only a viewer for whom `authorize_unredacted_request_evidence_access_with` returns true, with a `because:` | No | Policy `delete_after:` / `retain_until:`, else `config.delete_recorded_browser_user_agents_after`. Applied by `clickwrap:retention:apply` or `Clickwrap.delete_recorded_browser_user_agent!` | `{"state": "deleted_after_retention", "deleted_at": "..."}` |
|
|
108
|
+
| `browser_user_agent_was_client_supplied` | Always `true` | Set unconditionally when a user-agent is recorded | — it is the qualifier, not a claim | No | Same as the value | No | Kept | Unchanged |
|
|
109
|
+
| `browser_user_agent_recorded_at` | When the header was written | Server clock at capture | — | No | Same as the value | No | Kept | Unchanged |
|
|
110
|
+
| `browser_user_agent_delete_after` | The date this value became due for deletion | Written at capture | — | No | Same as the value | No | Kept | Unchanged |
|
|
111
|
+
| `browser_user_agent_retain_until_rule` | Named host retention calculation, when `retain_until:` was used | Written at capture | — | No | Same as the value | No | Kept | Unchanged |
|
|
112
|
+
| `browser_user_agent_deleted_at` | When the value was deleted | Set by the disposition | — | No | Same as the value | No | — | This is what makes the state `deleted_after_retention` |
|
|
113
|
+
| `browser_user_agent_unavailable_reason` | Why no header was stored: `no_http_request`, `capture_channel_carries_no_http_request`, or `no_browser_user_agent_on_http_request` | Set instead of a value | — | No | Anyone who can read the receipt | Yes, when the state is `unavailable` | — | — |
|
|
114
|
+
|
|
115
|
+
Browsers are actively reducing what this header says. Chrome's own documentation covers both
|
|
116
|
+
the legitimate uses and the fingerprinting risk, and describes reduced-granularity User-Agent
|
|
117
|
+
information alongside explicit higher-entropy Client Hints
|
|
118
|
+
([User-Agent Client Hints](https://developer.chrome.com/docs/privacy-security/user-agent-client-hints))
|
|
119
|
+
*(vendor documentation)*. Clickwrap records the raw header only. It requests no Client Hints,
|
|
120
|
+
and the signed presentation manifest — not this string — remains the evidence of what the
|
|
121
|
+
server offered.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## IP geolocation: the nine data fields
|
|
126
|
+
|
|
127
|
+
Each is a separate `record_ip_geolocation(...)` keyword and a separate line in the privacy
|
|
128
|
+
inventory, because each one is a separate decision about what to keep about somebody's network
|
|
129
|
+
context. `latitude_and_longitude` is one coupled choice on purpose: half a coordinate is not a
|
|
130
|
+
result.
|
|
131
|
+
|
|
132
|
+
Everything in this table shares four properties, so they are stated once rather than repeated
|
|
133
|
+
in every cell:
|
|
134
|
+
|
|
135
|
+
- **Where it comes from:** the configured `ip_geolocation_resolver` — one provider's estimate
|
|
136
|
+
about the observed IP address at resolution time. Nothing here is measured.
|
|
137
|
+
- **Encrypted:** yes, when `encrypt_recorded_ip_geolocation` is true, which is the default.
|
|
138
|
+
Every value column below is in `Clickwrap::RequestEvidence::ENCRYPTED_COLUMNS` and carries
|
|
139
|
+
ciphertext at rest — including the country code, which is lower precision than a coordinate
|
|
140
|
+
but is still personal data once it is attached to an identified actor and an event.
|
|
141
|
+
Coordinates are stored as strings rather than decimals for this reason and one other: a
|
|
142
|
+
receipt serializes them as strings anyway, so a decimal column would put a rounding step
|
|
143
|
+
between what the provider said and what the evidence shows.
|
|
144
|
+
|
|
145
|
+
The **provenance** columns beside them — provider name, provider source, database version and
|
|
146
|
+
digest, accuracy radius, estimated flag, resolution time, unavailable reason — are
|
|
147
|
+
deliberately *not* encrypted. They say how certain the values are rather than what they are,
|
|
148
|
+
they are what `clickwrap:doctor` and `Clickwrap::Privacy.inventory` read, and encrypting them
|
|
149
|
+
would hide the uncertainty while leaving the estimate itself just as sensitive.
|
|
150
|
+
- **Who can read it / in a default export:** only a viewer authorized for the
|
|
151
|
+
`ip_geolocation` category with a `because:`; never in a default export.
|
|
152
|
+
- **Deletion trigger and result:** one category, one deletion. Deleting IP geolocation nulls
|
|
153
|
+
every value column below at once and leaves all provenance intact, so the receipt reports
|
|
154
|
+
`{"state": "deleted_after_retention", "deleted_at": "..."}` rather than a gap.
|
|
155
|
+
|
|
156
|
+
| Field (policy keyword) | Columns it unlocks | What it is | Does not establish |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `country` | `ip_geolocation_country_code`, `ip_geolocation_country_name` | The country the provider associates with the address | Nationality, residence, applicable law, or where the person was. It must never select governing law, jurisdiction, terms, or eligibility on its own |
|
|
159
|
+
| `region` | `ip_geolocation_region_name`, `ip_geolocation_region_code` | The first-level subdivision the provider associates with the address | Presence in that region |
|
|
160
|
+
| `city` | `ip_geolocation_city_name` | The city the provider associates with the address | That anyone was in that city. A city result can describe a wide radius |
|
|
161
|
+
| `postal_code` | `ip_geolocation_postal_code` | The postal area the provider associates with the address | A street address or a household |
|
|
162
|
+
| `latitude_and_longitude` | `ip_geolocation_latitude`, `ip_geolocation_longitude` | Approximate coordinates for the address | GPS, device location, or a position. Read them with the accuracy radius or not at all |
|
|
163
|
+
| `timezone` | `ip_geolocation_timezone` | The timezone the provider reports for the address | The person's timezone or their device's clock setting |
|
|
164
|
+
| `continent` | `ip_geolocation_continent_code` | The continent code the provider reports | Anything the country field does not already fail to establish |
|
|
165
|
+
| `metro_code` | `ip_geolocation_metro_code` | The provider's metro/DMA code for the address | A place. It is a media-market identifier |
|
|
166
|
+
| `accuracy_radius_in_kilometers` | `ip_geolocation_accuracy_radius_in_kilometers` | The radius within which the provider expects the true address location to fall | Precision. A large radius is the provider being honest, not a defect |
|
|
167
|
+
|
|
168
|
+
MaxMind states expressly that GeoIP data cannot identify a household, an individual, or a
|
|
169
|
+
street address, and that accuracy varies by IP type, country, mobile network, VPN/proxy use,
|
|
170
|
+
and ISP practice
|
|
171
|
+
([geolocation accuracy](https://support.maxmind.com/knowledge-base/articles/maxmind-geolocation-accuracy),
|
|
172
|
+
[City database fields](https://dev.maxmind.com/geoip/docs/databases/city-and-country/city-binary/))
|
|
173
|
+
*(vendor documentation)*. Cloudflare describes its visitor-location headers as location
|
|
174
|
+
information for the visitor's **IP address**
|
|
175
|
+
([IP geolocation](https://developers.cloudflare.com/network/ip-geolocation/))
|
|
176
|
+
*(vendor documentation)*.
|
|
177
|
+
|
|
178
|
+
So a receipt may say:
|
|
179
|
+
|
|
180
|
+
> Estimated country associated with the observed IP address according to provider X at capture
|
|
181
|
+
> time.
|
|
182
|
+
|
|
183
|
+
It may not say:
|
|
184
|
+
|
|
185
|
+
> User location: Madrid.
|
|
186
|
+
|
|
187
|
+
A revealed receipt reports these as a single `ip_geolocation` object carrying only the
|
|
188
|
+
authorized fields, with `country` rendered from the country **code**, `region` from the region
|
|
189
|
+
code when there is one and the name otherwise, and `latitude_and_longitude` as a nested pair
|
|
190
|
+
that is omitted entirely unless both halves are present.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## IP geolocation: the provenance fields
|
|
195
|
+
|
|
196
|
+
These are not a policy choice. Any stored IP-geolocation result carries them, and so does any
|
|
197
|
+
failure to produce one. A country code with no provider behind it, or coordinates with no
|
|
198
|
+
resolution time, invites a reader to treat a guess about an address as a fact about a person.
|
|
199
|
+
|
|
200
|
+
| Field | What it is | Where it comes from | Does not establish | Encrypted | Who can read it | In a default export | Deletion trigger | After deletion the receipt says |
|
|
201
|
+
|---|---|---|---|---|---|---|---|---|
|
|
202
|
+
| `ip_geolocation_provider_name` | Which provider actually answered, e.g. `maxmind` or `cloudflare` | The resolver's own `provider_name`; the Trackdown adapter copies the answering provider even under `:auto` | That the provider was right | No | Authorized `ip_geolocation` viewers | No | Kept after deletion | Unchanged |
|
|
203
|
+
| `ip_geolocation_provider_source` | Which source answered, e.g. a MaxMind local database or Cloudflare request headers | The resolver's result; the Trackdown adapter copies its per-result `provider_source` | That the source was accurate or arrived over a trusted path | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
204
|
+
| `ip_geolocation_database_version` | The provider database build the answer came from | The resolver, when it exposes one | That the build was current | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
205
|
+
| `ip_geolocation_database_sha256` | A digest of that database build | The resolver, when it exposes one | That the digest was independently checked | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
206
|
+
| `ip_geolocation_accuracy_radius_confidence_percentage` | The confidence the provider attaches to its accuracy radius | The resolver, when it exposes one | Precision. It is the provider's own statistic about its own estimate | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
207
|
+
| `ip_geolocation_was_estimated` | Always `true` for a stored result | Set unconditionally: no IP-geolocation answer from any provider is an observation | — it is the qualifier | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
208
|
+
| `ip_geolocation_source_was_verified_by_host` | Whether the host's verifier vouched for the exact request path | The resolver's per-result trust state. Trackdown defaults request-backed results to unverified and calls the host's provider-specific verifier for each request | That the verifier was designed or deployed correctly. Clickwrap records the result; the host owns that proof | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
209
|
+
| `ip_geolocation_resolved_at` | When the estimate was produced | The resolver's own time, or the server clock at extraction when the resolver reports none | Anything about the person | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
210
|
+
| `ip_geolocation_unavailable_reason` | Why no estimate was stored | See the list below | — | No | Anyone who can read the receipt | Yes, when the state is `unavailable` | — | — |
|
|
211
|
+
| `ip_geolocation_recorded_at` | When the estimate was written | Server clock at capture | — | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
212
|
+
| `ip_geolocation_delete_after` / `ip_geolocation_retain_until_rule` | The schedule recorded at capture | Policy `delete_after:` / `retain_until:`, else the configuration default | — | No | Authorized `ip_geolocation` viewers | No | Kept | Unchanged |
|
|
213
|
+
| `ip_geolocation_deleted_at` | When the values were deleted | Set by the disposition | — | No | Authorized `ip_geolocation` viewers | No | — | This is what makes the state `deleted_after_retention` |
|
|
214
|
+
|
|
215
|
+
There are six distinct reasons an estimate can be missing, and they are kept apart because
|
|
216
|
+
they tell an auditor completely different things:
|
|
217
|
+
|
|
218
|
+
| Reason | What actually happened |
|
|
219
|
+
|---|---|
|
|
220
|
+
| the IP address's own unavailable reason | There was no address to resolve, so there was nothing to ask about |
|
|
221
|
+
| `ip_geolocation_resolver_raised_<ErrorClass>` | The resolver raised. The error **class** is recorded and the message never is, because a provider message can quote the address |
|
|
222
|
+
| `resolver_returned_no_result` | The resolver returned nothing at all |
|
|
223
|
+
| a reason supplied by the resolver | The resolver explained its own failure — the Trackdown adapter emits `no_ip_address_to_resolve`, `provider_returned_no_result`, `provider_supplied_no_location_fields`, or `trackdown_raised_<ErrorClass>` |
|
|
224
|
+
| `provider_supplied_no_authorized_field` | The provider answered, and had no value for any field this policy authorized |
|
|
225
|
+
| `resolver_cannot_supply_authorized_fields` | The provider can never supply the authorized fields. That is a configuration problem, not a fact about this address |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Enabling a field
|
|
230
|
+
|
|
231
|
+
Two places, and the policy always wins.
|
|
232
|
+
|
|
233
|
+
**In the initializer, for every policy.** Each field is its own setting, and each is `false`:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
Clickwrap.configure do |config|
|
|
237
|
+
config.record_ip_address_by_default = true
|
|
238
|
+
config.reason_for_recording_ip_addresses_by_default =
|
|
239
|
+
"Investigate account compromise and disputes about recorded actions"
|
|
240
|
+
config.legal_basis_reference_for_recording_ip_addresses_by_default = "LIA-SECURITY-2026-01"
|
|
241
|
+
config.delete_recorded_ip_addresses_after = 90.days
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Turning a default on without a purpose or without a deletion period is a `ConfigurationError`
|
|
246
|
+
at the end of the `configure` block, not a warning. Enabling any
|
|
247
|
+
`record_ip_geolocation_*_by_default` without an `ip_geolocation_resolver` is likewise a
|
|
248
|
+
`ConfigurationError` — there would be nothing to resolve them.
|
|
249
|
+
|
|
250
|
+
**In one policy, for one flow.** This is the shape most applications want: ordinary signup
|
|
251
|
+
inherits nothing, and the one consequential action opts in by name.
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
Clickwrap.policy :regulated_authorization do
|
|
255
|
+
authorize :regulated_action, one_time: true, valid_for: 10.minutes
|
|
256
|
+
|
|
257
|
+
review_request_evidence_configuration_on Date.new(2027, 8, 15)
|
|
258
|
+
|
|
259
|
+
record_ip_address(
|
|
260
|
+
encrypted: true,
|
|
261
|
+
retain_until: :security_evidence_retention_ends,
|
|
262
|
+
because: "Investigate account compromise and disputes about this action",
|
|
263
|
+
legal_basis_reference: "LIA-SECURITY-2026-01"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
record_browser_user_agent(
|
|
267
|
+
encrypted: true,
|
|
268
|
+
retain_until: :security_evidence_retention_ends,
|
|
269
|
+
because: "Corroborate the client context used for this action",
|
|
270
|
+
legal_basis_reference: "LIA-SECURITY-2026-01"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
record_ip_geolocation(
|
|
274
|
+
country: true,
|
|
275
|
+
region: true,
|
|
276
|
+
city: true,
|
|
277
|
+
postal_code: false,
|
|
278
|
+
latitude_and_longitude: true,
|
|
279
|
+
timezone: false,
|
|
280
|
+
continent: false,
|
|
281
|
+
metro_code: false,
|
|
282
|
+
accuracy_radius_in_kilometers: true,
|
|
283
|
+
retain_until: :security_evidence_retention_ends,
|
|
284
|
+
because: "Corroborate anomalous access and investigate action disputes",
|
|
285
|
+
legal_basis_reference: "LIA-SECURITY-2026-01",
|
|
286
|
+
data_protection_impact_assessment_reference: "DPIA-2026-04"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
retain_with :regulated_evidence
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Every keyword there is doing work:
|
|
294
|
+
|
|
295
|
+
- **`because:`** is the present purpose, in a sentence someone outside engineering can read. It
|
|
296
|
+
is stored, printed by `bin/rails clickwrap:privacy:inventory`, and required.
|
|
297
|
+
- **`legal_basis_reference:`** and **`data_protection_impact_assessment_reference:`** are
|
|
298
|
+
host-supplied pointers to your own reviewed documents. Clickwrap stores them. It does not
|
|
299
|
+
read them, validate them, or endorse them.
|
|
300
|
+
- **`delete_after:`** or **`retain_until:`** is not optional. If a policy records a field and
|
|
301
|
+
neither the policy nor the configuration says when it goes away, capture raises a
|
|
302
|
+
`ConfigurationError` before the row is written. There is no keep-forever default in this gem.
|
|
303
|
+
- **`fail_if_unavailable:`** (default `false`) decides whether evidence you cannot get is worse
|
|
304
|
+
than no capture at all. When it is `true` and the field cannot be resolved, the capture and
|
|
305
|
+
the protected action roll back together.
|
|
306
|
+
- **`review_request_evidence_configuration_on`** is a date by which someone should look at this
|
|
307
|
+
again. `bin/rails clickwrap:doctor` reports policies that collect personal data without one,
|
|
308
|
+
and policies whose date has passed.
|
|
309
|
+
|
|
310
|
+
Two things you cannot do, by construction: a browser cannot submit or replace any of these
|
|
311
|
+
values, and the form helper renders no hidden field carrying an IP address, user-agent,
|
|
312
|
+
geolocation field, policy version, validity window, or retention rule. A native or API client
|
|
313
|
+
may send a separately labeled `client_reported_*` value only where a host adapter permits it,
|
|
314
|
+
and it stays labeled that way forever.
|
|
315
|
+
|
|
316
|
+
Confirm what a configuration actually does before you trust it:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
bin/rails clickwrap:privacy:inventory
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Trusted proxies, and why `request.remote_ip` alone is not enough
|
|
325
|
+
|
|
326
|
+
Clickwrap's default reader is `request.remote_ip`. That is the conventional Rails answer and
|
|
327
|
+
the right starting point, but the value it returns is only as good as the proxy topology Rails
|
|
328
|
+
was told about.
|
|
329
|
+
|
|
330
|
+
`ActionDispatch::RemoteIp` inspects potentially forwarded headers, discards addresses matching
|
|
331
|
+
the configured trusted proxies, and performs a spoof check — and Rails documents that the
|
|
332
|
+
middleware can be wrong when the deployment does not match the proxy behavior it assumes
|
|
333
|
+
([`ActionDispatch::RemoteIp`](https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html))
|
|
334
|
+
*(vendor documentation)*. If your origin is reachable directly, a client can simply send an
|
|
335
|
+
`X-Forwarded-For` header of its choosing, and nothing downstream can tell the difference.
|
|
336
|
+
|
|
337
|
+
So "we record IP addresses" is not one decision. It is four:
|
|
338
|
+
|
|
339
|
+
1. **Configure the proxies.** Set `config.action_dispatch.trusted_proxies` to the actual
|
|
340
|
+
ranges your load balancer, CDN, or mesh uses. The default trusts private ranges only.
|
|
341
|
+
2. **Close the side door.** If requests can reach your application without passing through
|
|
342
|
+
that infrastructure, the header is attacker-controlled no matter what Rails is configured
|
|
343
|
+
with. Block direct origin access, or have trusted infrastructure strip and re-set the
|
|
344
|
+
headers before your application sees them.
|
|
345
|
+
3. **Test it.** Send a request with a forged `X-Forwarded-For` through your real path and
|
|
346
|
+
assert on the address Clickwrap stored. The gem's own suite has fixtures for exactly this;
|
|
347
|
+
yours should too, because the failure is silent.
|
|
348
|
+
4. **Record which configuration was in force.** Set
|
|
349
|
+
`config.trusted_proxy_configuration_digest` from the effective rules themselves:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
config.trusted_proxy_configuration_digest =
|
|
353
|
+
Clickwrap.trusted_proxy_configuration_digest_for_rails_application
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
This uses `config.action_dispatch.trusted_proxies`, or Rails' actual default rules when the
|
|
357
|
+
application has not overridden them. A prose description such as "our Cloudflare setup" is
|
|
358
|
+
not configuration provenance. The digest still does not make the setup correct. It lets a
|
|
359
|
+
later reader identify which rules were in force — the difference between corroborating
|
|
360
|
+
evidence and a number with no recorded collection context.
|
|
361
|
+
|
|
362
|
+
If you replace the reader, you own that decision, and the receipt says so: any host-assigned
|
|
363
|
+
lambda is labeled `host_configured_reader` rather than `rails_request_remote_ip`, even when the
|
|
364
|
+
body is identical. Clickwrap will not claim Rails' spoof checks on your behalf.
|
|
365
|
+
|
|
366
|
+
Finally: Clickwrap does not store an `X-Forwarded-For` chain, and refuses a reader that returns
|
|
367
|
+
one. If your topology genuinely needs a different address than `remote_ip` gives you, pick it
|
|
368
|
+
in your own reader and pick exactly one.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Trackdown
|
|
373
|
+
|
|
374
|
+
`trackdown` is the optional official IP-geolocation resolver. It is not a dependency of this
|
|
375
|
+
gem and must never become one.
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
bundle add trackdown --version ">= 0.4"
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
# clickwrap-doc-test: syntax-only — requires the optional trackdown gem installed above
|
|
383
|
+
Trackdown.configure do |trackdown|
|
|
384
|
+
# This flag must be set by infrastructure that actually authenticated or
|
|
385
|
+
# allowlisted the Cloudflare-to-origin path. It must not come from CF-* header
|
|
386
|
+
# presence, because a client reaching the origin can send those headers too.
|
|
387
|
+
trackdown.verify_request_came_through_trusted_cloudflare_path_with do |request|
|
|
388
|
+
request.env["my_app.cloudflare_origin_was_verified"] == true
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
Clickwrap.configure do |config|
|
|
393
|
+
config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
|
|
394
|
+
end
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The adapter is where the evidence discipline lives:
|
|
398
|
+
|
|
399
|
+
- **Only the policy's allowlist is persisted.** Trackdown's `to_h` includes `country_info`, the
|
|
400
|
+
whole ISO3166 country record — reasonable for a general geolocation gem, wrong for evidence.
|
|
401
|
+
The adapter copies named fields one at a time and the extractor then keeps only the subset
|
|
402
|
+
the server-owned policy authorized.
|
|
403
|
+
- **Placeholders are mapped back to nothing.** Trackdown returns the string `"Unknown"` for a
|
|
404
|
+
country or city it could not determine, and Cloudflare's own "no country" code is `"XX"`.
|
|
405
|
+
Written into a receipt those would be indistinguishable from a country a provider actually
|
|
406
|
+
reported, so they become `nil` and the result is recorded as unavailable with a reason.
|
|
407
|
+
- **The minimum version fails loudly.** Trackdown 0.4 is required because it supplies
|
|
408
|
+
per-result provider, source, resolution time, uncertainty, database provenance, and
|
|
409
|
+
per-request host trust. An older loaded version is refused at the initializer line instead
|
|
410
|
+
of making Clickwrap invent those facts.
|
|
411
|
+
- **Every optional field read is defensive.** A field one Trackdown provider cannot supply is
|
|
412
|
+
`nil` instead of an exception in the middle of a capture.
|
|
413
|
+
- **`estimated` is always true.** No provider's answer is an observation of where anybody was.
|
|
414
|
+
- **Trust is per request, never process-wide.** Clickwrap passes the exact `http_request` from
|
|
415
|
+
the protected capture to `Trackdown.locate`. Trackdown runs the provider-specific host
|
|
416
|
+
verifier on that request and returns `source_was_verified_by_host?`; the adapter copies that
|
|
417
|
+
answer. The former experimental constructor-wide boolean is refused because it could bless
|
|
418
|
+
direct-origin requests after one deployment claim.
|
|
419
|
+
- **Cloudflare header presence is not trust.** Trackdown's
|
|
420
|
+
[Cloudflare provider](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/cloudflare_provider.rb)
|
|
421
|
+
reads request environment values, while its
|
|
422
|
+
[configuration](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/configuration.rb)
|
|
423
|
+
leaves results unverified unless the host's callback vouches for that request. Follow
|
|
424
|
+
Trackdown's exact
|
|
425
|
+
[origin-protection guidance](https://github.com/rameerez/trackdown/blob/v0.4.0/README.md#did-the-request-really-come-through-your-cdn);
|
|
426
|
+
`CF-*` header presence alone is never the callback.
|
|
427
|
+
|
|
428
|
+
Trackdown 0.4's
|
|
429
|
+
[result object](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/location_result.rb)
|
|
430
|
+
exposes the answering provider and source, resolution time, explicit availability and estimate
|
|
431
|
+
state, MaxMind accuracy radius and database build metadata, and per-request source trust. The
|
|
432
|
+
work was reviewed in [Trackdown PR #9](https://github.com/rameerez/trackdown/pull/9) and closed
|
|
433
|
+
the contract scoped in [Trackdown issue #8](https://github.com/rameerez/trackdown/issues/8).
|
|
434
|
+
The adapter copies only named fields. It serializes the exact MaxMind build epoch as
|
|
435
|
+
`database_build_epoch:<integer>` and normalizes a 64-hex database digest to Clickwrap's
|
|
436
|
+
`sha256:<hex>` form.
|
|
437
|
+
|
|
438
|
+
**Other providers.** `config.ip_geolocation_resolver` accepts anything responding to
|
|
439
|
+
`#resolve(ip_address, http_request: nil)` and `#capabilities`;
|
|
440
|
+
`Clickwrap::IpGeolocation::Resolver` is the full contract. The explicit request keyword keeps
|
|
441
|
+
request-backed provenance from being discarded by an adapter. A resolver that does not need
|
|
442
|
+
the request still accepts and ignores it. For tests, use
|
|
443
|
+
`Clickwrap::IpGeolocation::StaticResolver`, which answers from a fixed table — a real provider
|
|
444
|
+
makes request-evidence assertions fail because a database was rebuilt rather than because the
|
|
445
|
+
allowlist changed.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Footprinted is analytics, not the evidence backend
|
|
450
|
+
|
|
451
|
+
`footprinted` is a good analytics gem and the wrong place for authoritative assent evidence.
|
|
452
|
+
That is a statement about defaults, not about quality. At pinned commit `03b714bd` it:
|
|
453
|
+
|
|
454
|
+
- requires an IP address and automatically expands it into country, city, region, timezone,
|
|
455
|
+
latitude, and longitude
|
|
456
|
+
([model](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/footprinted/footprint.rb#L7-L52));
|
|
457
|
+
- permits asynchronous event creation
|
|
458
|
+
([tracking concern](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/footprinted/model.rb#L21-L65));
|
|
459
|
+
- rescues geolocation failures and proceeds
|
|
460
|
+
([model](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/footprinted/footprint.rb#L38-L52));
|
|
461
|
+
- accepts arbitrary JSON metadata and stores broad geolocation columns
|
|
462
|
+
([migration template](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/generators/footprinted/templates/create_footprinted_footprints.rb.erb#L5-L31)); and
|
|
463
|
+
- destroys footprints with the tracked parent
|
|
464
|
+
([association](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/footprinted/model.rb#L7-L9)).
|
|
465
|
+
|
|
466
|
+
All *(pinned source code)*.
|
|
467
|
+
|
|
468
|
+
Line them up against what evidence needs and the mismatch is structural, not stylistic:
|
|
469
|
+
|
|
470
|
+
| Footprinted behavior | What evidence requires instead |
|
|
471
|
+
|---|---|
|
|
472
|
+
| An IP address is required | Every field is off until a policy names it, and an event with no request evidence at all is normal |
|
|
473
|
+
| Enabling tracking enables broad geolocation | Nine fields, nine separate decisions, each with its own purpose and retention rule |
|
|
474
|
+
| Writes may be asynchronous | Required request evidence resolves before the transaction and commits inside it, or the protected action rolls back |
|
|
475
|
+
| Geolocation failure is rescued and the write proceeds | Failure is recorded as an explicit `unavailable` state with a reason, and a policy may choose to fail closed |
|
|
476
|
+
| Footprints are destroyed with the tracked parent | Deleting an actor never cascades evidence; disposition is planned, reviewed, hold-aware, and itself recorded |
|
|
477
|
+
|
|
478
|
+
What is fine, and what the design actually anticipates, is a sanitized analytics copy emitted
|
|
479
|
+
after commit — an event ID, a policy key, a kind, perhaps an explicitly approved country code:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
config.after_event_is_committed = lambda do |event|
|
|
483
|
+
Analytics::ClickwrapEventJob.perform_later(event.id, event.policy_key)
|
|
484
|
+
end
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
That copy is never the evidence that authorized the protected action, and an analytics failure
|
|
488
|
+
can never undo or stand in for the Clickwrap event. Clickwrap ships no Footprinted adapter.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Sources
|
|
493
|
+
|
|
494
|
+
| Source | Class |
|
|
495
|
+
|---|---|
|
|
496
|
+
| [Breyer, Case C-582/14](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A62014CJ0582) | Case |
|
|
497
|
+
| [Berman v. Freedom Financial Network](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/05/20-16900.pdf) | Case |
|
|
498
|
+
| [Tejon v. Zeus Networks](https://media.ca11.uscourts.gov/opinions/pub/files/202411114.pdf) | Case |
|
|
499
|
+
| [Toth v. Everly Well](https://www.ca1.uscourts.gov/sites/ca1/files/opnfiles/23-1727P-01A.pdf) | Case |
|
|
500
|
+
| [GDPR Article 5](https://eur-lex.europa.eu/eli/reg/2016/679/art_5/oj/eng), [13](https://eur-lex.europa.eu/eli/reg/2016/679/art_13/oj/eng), [25](https://eur-lex.europa.eu/eli/reg/2016/679/art_25/oj/eng), [32](https://eur-lex.europa.eu/eli/reg/2016/679/art_32/oj/eng) | Law |
|
|
501
|
+
| [Federal Rule of Evidence 901](https://www.law.cornell.edu/rules/fre/rule_901) | Law |
|
|
502
|
+
| [EDPB Guidelines 2/2023 on the technical scope of ePrivacy Article 5(3)](https://www.edpb.europa.eu/system/files/documents/2024-10/edpb_guidelines_202302_technical_scope_art_53_eprivacydirective_v2_en_0.pdf) | Regulator guidance |
|
|
503
|
+
| [MaxMind geolocation accuracy](https://support.maxmind.com/knowledge-base/articles/maxmind-geolocation-accuracy) | Vendor documentation |
|
|
504
|
+
| [MaxMind City database fields](https://dev.maxmind.com/geoip/docs/databases/city-and-country/city-binary/) | Vendor documentation |
|
|
505
|
+
| [Cloudflare IP geolocation](https://developers.cloudflare.com/network/ip-geolocation/) | Vendor documentation |
|
|
506
|
+
| [Chrome User-Agent Client Hints](https://developer.chrome.com/docs/privacy-security/user-agent-client-hints) | Vendor documentation |
|
|
507
|
+
| [`ActionDispatch::RemoteIp`](https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html) | Vendor documentation |
|
|
508
|
+
| [Ironclad bulk retrieval, `connection_data.remote_address`](https://clickwrap-developer.ironcladapp.com/docs/retrieving-data-in-bulk) | Vendor documentation |
|
|
509
|
+
| [RFC 791](https://www.rfc-editor.org/info/rfc791/) — IPv4 addresses are 32 bits, which is why the annex binding digest is keyed rather than a plain hash | Technical standard |
|
|
510
|
+
| [Trackdown v0.4.0 result object](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/location_result.rb), [configuration](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/configuration.rb), [Cloudflare provider](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/cloudflare_provider.rb), [MaxMind provider](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/maxmind_provider.rb), [PR #9](https://github.com/rameerez/trackdown/pull/9) | Pinned released source and project change record |
|
|
511
|
+
| [Footprinted pinned model](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/footprinted/footprint.rb#L7-L52), [tracking concern](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/footprinted/model.rb#L7-L65) | Pinned source code |
|
|
512
|
+
| The field selection, the ordering of evidentiary priority, and every API prescription above | Product-design inference |
|