clickwrap 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +90 -0
  3. data/CHANGELOG.md +612 -0
  4. data/README.md +830 -1204
  5. data/SECURITY.md +33 -0
  6. data/app/assets/stylesheets/clickwrap.css +241 -0
  7. data/app/controllers/clickwrap/application_controller.rb +79 -0
  8. data/app/controllers/clickwrap/captures_controller.rb +145 -0
  9. data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
  10. data/app/controllers/clickwrap/receipts_controller.rb +115 -0
  11. data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
  12. data/app/helpers/clickwrap/engine_helper.rb +97 -0
  13. data/app/views/clickwrap/captures/show.html.erb +34 -0
  14. data/app/views/clickwrap/receipts/index.html.erb +38 -0
  15. data/app/views/clickwrap/receipts/show.html.erb +91 -0
  16. data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
  17. data/app/views/clickwrap/shared/_fields.html.erb +100 -0
  18. data/app/views/clickwrap/shared/_statement.html.erb +105 -0
  19. data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
  20. data/config/locales/en.yml +160 -0
  21. data/config/locales/es.yml +138 -0
  22. data/config/routes.rb +41 -0
  23. data/exe/clickwrap +374 -0
  24. data/guides/README.md +30 -0
  25. data/guides/accessibility.md +249 -0
  26. data/guides/consent-and-lifecycle.md +299 -0
  27. data/guides/integrating.md +610 -0
  28. data/guides/integrity.md +212 -0
  29. data/guides/migrating.md +335 -0
  30. data/guides/naming.md +320 -0
  31. data/guides/organizations.md +320 -0
  32. data/guides/receipts-and-verification.md +415 -0
  33. data/guides/request-evidence.md +512 -0
  34. data/guides/retention-and-legal-holds.md +438 -0
  35. data/lib/clickwrap/actor_proxy.rb +147 -0
  36. data/lib/clickwrap/anonymous_actor.rb +47 -0
  37. data/lib/clickwrap/authority.rb +174 -0
  38. data/lib/clickwrap/canonical_json.rb +216 -0
  39. data/lib/clickwrap/capture/event_builder.rb +220 -0
  40. data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
  41. data/lib/clickwrap/capture.rb +650 -0
  42. data/lib/clickwrap/configuration.rb +1129 -0
  43. data/lib/clickwrap/controller_helpers.rb +758 -0
  44. data/lib/clickwrap/current_state.rb +282 -0
  45. data/lib/clickwrap/digest.rb +125 -0
  46. data/lib/clickwrap/doctor.rb +418 -0
  47. data/lib/clickwrap/document_definition.rb +255 -0
  48. data/lib/clickwrap/document_renderer.rb +83 -0
  49. data/lib/clickwrap/document_renderers/markdown.rb +175 -0
  50. data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
  51. data/lib/clickwrap/dsl/policy_builder.rb +462 -0
  52. data/lib/clickwrap/dsl/retention_builder.rb +89 -0
  53. data/lib/clickwrap/durable_commit_callback.rb +37 -0
  54. data/lib/clickwrap/engine.rb +184 -0
  55. data/lib/clickwrap/errors.rb +181 -0
  56. data/lib/clickwrap/form_builder_extensions.rb +341 -0
  57. data/lib/clickwrap/front_matter.rb +67 -0
  58. data/lib/clickwrap/identifier.rb +112 -0
  59. data/lib/clickwrap/import/external_receipt.rb +241 -0
  60. data/lib/clickwrap/import/fine_print.rb +290 -0
  61. data/lib/clickwrap/import/legacy.rb +450 -0
  62. data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
  63. data/lib/clickwrap/integrity/anchor.rb +130 -0
  64. data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
  65. data/lib/clickwrap/integrity/attestor.rb +221 -0
  66. data/lib/clickwrap/integrity/chain.rb +313 -0
  67. data/lib/clickwrap/integrity/timestamp.rb +143 -0
  68. data/lib/clickwrap/ip_geolocation/location.rb +112 -0
  69. data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
  70. data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
  71. data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
  72. data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
  73. data/lib/clickwrap/ip_geolocation.rb +16 -0
  74. data/lib/clickwrap/lifecycle.rb +534 -0
  75. data/lib/clickwrap/linter.rb +382 -0
  76. data/lib/clickwrap/localized_text.rb +101 -0
  77. data/lib/clickwrap/macros.rb +203 -0
  78. data/lib/clickwrap/models/application_record.rb +20 -0
  79. data/lib/clickwrap/models/chain_head.rb +79 -0
  80. data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
  81. data/lib/clickwrap/models/disposition_plan.rb +208 -0
  82. data/lib/clickwrap/models/document.rb +46 -0
  83. data/lib/clickwrap/models/document_version.rb +163 -0
  84. data/lib/clickwrap/models/event.rb +743 -0
  85. data/lib/clickwrap/models/event_document.rb +79 -0
  86. data/lib/clickwrap/models/event_statement.rb +92 -0
  87. data/lib/clickwrap/models/external_action.rb +150 -0
  88. data/lib/clickwrap/models/integrity_attestation.rb +90 -0
  89. data/lib/clickwrap/models/legal_hold.rb +81 -0
  90. data/lib/clickwrap/models/policy_revision.rb +115 -0
  91. data/lib/clickwrap/models/presentation.rb +59 -0
  92. data/lib/clickwrap/models/receipt_access.rb +53 -0
  93. data/lib/clickwrap/models/recording_sequence.rb +21 -0
  94. data/lib/clickwrap/models/request_evidence.rb +378 -0
  95. data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
  96. data/lib/clickwrap/models/statement_state.rb +130 -0
  97. data/lib/clickwrap/pending_receipt.rb +177 -0
  98. data/lib/clickwrap/policy.rb +283 -0
  99. data/lib/clickwrap/presentation_manifest.rb +210 -0
  100. data/lib/clickwrap/presenter.rb +716 -0
  101. data/lib/clickwrap/privacy.rb +419 -0
  102. data/lib/clickwrap/protected_outcome.rb +120 -0
  103. data/lib/clickwrap/receipt.rb +606 -0
  104. data/lib/clickwrap/receipt_html.rb +235 -0
  105. data/lib/clickwrap/receipt_verifier.rb +978 -0
  106. data/lib/clickwrap/reference.rb +44 -0
  107. data/lib/clickwrap/registration.rb +236 -0
  108. data/lib/clickwrap/registry.rb +54 -0
  109. data/lib/clickwrap/remediation_token.rb +155 -0
  110. data/lib/clickwrap/request_evidence_extractor.rb +590 -0
  111. data/lib/clickwrap/request_evidence_policy.rb +261 -0
  112. data/lib/clickwrap/retention/applier.rb +231 -0
  113. data/lib/clickwrap/retention/disposition.rb +221 -0
  114. data/lib/clickwrap/retention/planner.rb +502 -0
  115. data/lib/clickwrap/retention_class.rb +97 -0
  116. data/lib/clickwrap/reviewed_text.rb +28 -0
  117. data/lib/clickwrap/schema_requirements.rb +196 -0
  118. data/lib/clickwrap/services/authorize_external_action.rb +149 -0
  119. data/lib/clickwrap/services/load_policies.rb +69 -0
  120. data/lib/clickwrap/services/publish_documents.rb +251 -0
  121. data/lib/clickwrap/services/validate_policy_references.rb +166 -0
  122. data/lib/clickwrap/statement.rb +248 -0
  123. data/lib/clickwrap/subject_fingerprint.rb +28 -0
  124. data/lib/clickwrap/submission.rb +169 -0
  125. data/lib/clickwrap/system_actor.rb +31 -0
  126. data/lib/clickwrap/test_helpers.rb +676 -0
  127. data/lib/clickwrap/testing.rb +211 -0
  128. data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
  129. data/lib/clickwrap/verification.rb +504 -0
  130. data/lib/clickwrap/version.rb +12 -1
  131. data/lib/clickwrap/view_helpers.rb +190 -0
  132. data/lib/clickwrap/vocabulary.rb +294 -0
  133. data/lib/clickwrap.rb +497 -7
  134. data/lib/generators/clickwrap/document_generator.rb +164 -0
  135. data/lib/generators/clickwrap/hardening_generator.rb +177 -0
  136. data/lib/generators/clickwrap/install_generator.rb +1287 -0
  137. data/lib/generators/clickwrap/link_generator.rb +56 -0
  138. data/lib/generators/clickwrap/policy_generator.rb +118 -0
  139. data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
  140. data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
  141. data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
  142. data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
  143. data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
  144. data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
  145. data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
  146. data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
  147. data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
  148. data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
  149. data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
  150. data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
  151. data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
  152. data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
  153. data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
  154. data/lib/generators/clickwrap/views_generator.rb +101 -0
  155. data/lib/tasks/clickwrap.rake +569 -0
  156. metadata +311 -16
@@ -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 |