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,330 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/version"
4
+
5
+ module Clickwrap
6
+ module IpGeolocation
7
+ # The optional official adapter for the `trackdown` gem.
8
+ #
9
+ # bundle add trackdown --version ">= 0.4"
10
+ #
11
+ # Clickwrap.configure do |config|
12
+ # config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
13
+ # end
14
+ #
15
+ # `trackdown` is NOT a dependency of this gem and must never become one. It
16
+ # is required lazily, inside the constructor, so that a host who never names
17
+ # this class never loads it — and so that a host who does name it without
18
+ # installing it gets one sentence telling them what to do, at the line in
19
+ # their initializer that asked for it, instead of a NameError somewhere in
20
+ # the middle of a capture.
21
+ #
22
+ # Two rules shape the mapping below, and both exist because this adapter
23
+ # sits between a general-purpose geolocation gem and an evidence record that
24
+ # has to still be readable and honest in several years.
25
+ #
26
+ # ONE: every field is read through `respond_to?`. Trackdown's result object
27
+ # has gained fields across releases and will gain more; a `NoMethodError`
28
+ # during a capture would roll back the protected action for a field the
29
+ # policy may not even have authorized. Reading defensively means a newer
30
+ # Trackdown supplying an accuracy radius is picked up here with no change,
31
+ # and an older one simply reports nil.
32
+ #
33
+ # TWO: `to_h` is never persisted. Trackdown's `to_h` includes `country_info`
34
+ # — the whole ISO3166 country record — and a general gem is right to offer
35
+ # it. Copying it into evidence would store data no policy authorized and no
36
+ # receipt could explain. Clickwrap copies named fields, one at a time, and
37
+ # the extractor then keeps only the subset the server-owned policy allowed.
38
+ #
39
+ # Pinned sources for the mapping (released tag, not a moving branch):
40
+ # result object -> https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/location_result.rb
41
+ # Cloudflare -> https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/cloudflare_provider.rb
42
+ # MaxMind -> https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/maxmind_provider.rb
43
+ # Trackdown 0.4.0 closed the provenance gap scoped in
44
+ # https://github.com/rameerez/trackdown/issues/8. This adapter requires that
45
+ # release rather than silently manufacturing the missing facts itself.
46
+ class TrackdownResolver < Resolver
47
+ PROVIDER_NAME = "trackdown"
48
+ MINIMUM_TRACKDOWN_VERSION = Gem::Version.new("0.4.0")
49
+
50
+ # Trackdown returns the string "Unknown" for a country or city it could
51
+ # not determine, and Cloudflare's own "no country" code is "XX"
52
+ # (pinned provider source above). Neither is a place. Written into a
53
+ # receipt they would be indistinguishable from a country a provider
54
+ # actually reported, so they are mapped back to nil and the extractor
55
+ # records "the provider supplied no authorized field" instead. This is the
56
+ # single most important line in this file.
57
+ PLACEHOLDER_VALUES = ["unknown", "n/a", "xx", "-"].freeze
58
+
59
+ # What Trackdown could supply at all, by Clickwrap field name. Read
60
+ # dynamically from the installed result object where possible, so a
61
+ # Trackdown release that adds accuracy radius starts reporting the
62
+ # capability without an edit here.
63
+ FIELD_READERS = {
64
+ country: %i[country_code country_name],
65
+ region: %i[region region_name region_code],
66
+ city: %i[city city_name],
67
+ postal_code: %i[postal_code],
68
+ latitude_and_longitude: %i[latitude longitude],
69
+ timezone: %i[timezone time_zone],
70
+ continent: %i[continent continent_code],
71
+ metro_code: %i[metro_code],
72
+ accuracy_radius_in_kilometers: %i[accuracy_radius accuracy_radius_in_kilometers]
73
+ }.freeze
74
+
75
+ # What Trackdown 0.4 can supply across its providers, used only when the
76
+ # result class cannot be inspected. Cloudflare does not supply an accuracy
77
+ # radius, while MaxMind does; with Trackdown's default :auto provider the
78
+ # adapter can therefore supply it even though any one lookup may not.
79
+ PINNED_CAPABILITIES = %i[
80
+ country region city postal_code latitude_and_longitude timezone continent metro_code
81
+ accuracy_radius_in_kilometers
82
+ ].freeze
83
+
84
+ attr_reader :capabilities
85
+
86
+ # Trust is per request in Trackdown 0.4. A host registers its verifier with
87
+ # Trackdown, Trackdown runs it against the same request that supplied the
88
+ # CDN headers, and this adapter copies the result's explicit trust state.
89
+ # A constructor-wide boolean would overclaim every request after one
90
+ # deployment assertion, so the old experimental option is refused by name.
91
+ def initialize(provider_source: nil, source_verified_by_host: nil)
92
+ super()
93
+ require "trackdown" unless defined?(::Trackdown)
94
+ ensure_supported_trackdown_version!
95
+
96
+ unless source_verified_by_host.nil?
97
+ raise ConfigurationError,
98
+ "TrackdownResolver no longer accepts `source_verified_by_host:`. Trackdown " \
99
+ "0.4 verifies source trust per request. Configure " \
100
+ "`Trackdown.configuration.verify_request_came_through_trusted_cloudflare_path_with` " \
101
+ "or the matching CloudFront helper; the resolver will record the result's " \
102
+ "`source_was_verified_by_host?` value."
103
+ end
104
+
105
+ @provider_source_fallback = (provider_source || configured_provider_source).to_s
106
+ @capabilities = detect_capabilities.freeze
107
+
108
+ freeze
109
+ rescue ::LoadError => error
110
+ raise ConfigurationError,
111
+ "Clickwrap::IpGeolocation::TrackdownResolver needs the `trackdown` gem, which is " \
112
+ "not installed. Run `bundle add trackdown --version \">= 0.4\"` and configure " \
113
+ "it (it needs either a " \
114
+ "MaxMind database or Cloudflare visitor-location headers), or set " \
115
+ "`config.ip_geolocation_resolver = nil` and turn off the IP-geolocation fields " \
116
+ "your policies enable. The underlying load error was: #{error.message}"
117
+ end
118
+
119
+ def resolve(ip_address, http_request: nil)
120
+ address = ip_address.to_s.strip
121
+ return unavailable("no_ip_address_to_resolve") if address.empty?
122
+
123
+ result = ::Trackdown.locate(address, request: http_request)
124
+ return unavailable("provider_returned_no_result") if result.nil?
125
+
126
+ if result.respond_to?(:unavailable?) && result.unavailable?
127
+ return unavailable(
128
+ text(result, :unavailable_reason) || "provider_returned_unavailable",
129
+ result:
130
+ )
131
+ end
132
+
133
+ location = build_location(result)
134
+
135
+ # Trackdown answers with a result object full of "Unknown" when no
136
+ # provider is configured, when its database has no row for the address,
137
+ # and when a Cloudflare country header says "XX". Once the placeholders
138
+ # are mapped away that is an empty answer, and an empty answer is an
139
+ # unavailable result with a reason on it — not a location whose every
140
+ # field happens to be blank.
141
+ return unavailable("provider_supplied_no_location_fields", result:) unless location.any_data_field?
142
+
143
+ location
144
+ rescue StandardError => error
145
+ # Trackdown raises for ordinary conditions — a private or loopback
146
+ # address in development, a missing MaxMind database, a lookup timeout.
147
+ # None of those should abort a capture by itself: the policy decides
148
+ # whether unavailable IP geolocation is fatal. The reason carries the
149
+ # error CLASS and never the message, because a provider message can
150
+ # quote the IP address and this string is written to a column that a
151
+ # redacted receipt is allowed to show.
152
+ unavailable("trackdown_raised_#{error.class}")
153
+ end
154
+
155
+ private
156
+
157
+ def unavailable(reason, result: nil)
158
+ Location.unavailable(
159
+ reason:,
160
+ provider_name: provider_name(result),
161
+ provider_source: provider_source(result),
162
+ database_version: database_version(result),
163
+ database_sha256: database_sha256(result),
164
+ estimated: true,
165
+ source_was_verified_by_host: source_was_verified_by_host?(result),
166
+ resolved_at: resolved_at(result)
167
+ )
168
+ end
169
+
170
+ def build_location(result)
171
+ Location.new(
172
+ country_code: text(result, :country_code),
173
+ country_name: text(result, :country_name),
174
+ region_name: text(result, :region, :region_name),
175
+ region_code: text(result, :region_code),
176
+ city_name: text(result, :city, :city_name),
177
+ postal_code: text(result, :postal_code),
178
+ latitude: number(result, :latitude),
179
+ longitude: number(result, :longitude),
180
+ timezone: text(result, :timezone, :time_zone),
181
+ # Both Trackdown providers put a two-letter code in `continent`
182
+ # (MaxMind's `continent.code`, Cloudflare's `CF-IPContinent`), which
183
+ # is why it maps to the continent CODE column rather than a name.
184
+ continent_code: text(result, :continent, :continent_code),
185
+ metro_code: text(result, :metro_code),
186
+ provider_name: provider_name(result),
187
+ provider_source: provider_source(result),
188
+ database_version: database_version(result),
189
+ database_sha256: database_sha256(result),
190
+ accuracy_radius_in_kilometers: number(result, :accuracy_radius_in_kilometers, :accuracy_radius),
191
+ accuracy_radius_confidence_percentage: number(result, :accuracy_radius_confidence_percentage),
192
+ # Always. No IP geolocation result from any provider is an
193
+ # observation of where anyone was; it is an estimate about an address,
194
+ # and this flag is what keeps a receipt from implying otherwise.
195
+ estimated: true,
196
+ source_was_verified_by_host: source_was_verified_by_host?(result),
197
+ resolved_at: resolved_at(result)
198
+ )
199
+ end
200
+
201
+ # Reads the first reader the result object actually has. Returns nil for a
202
+ # missing reader, a blank value, or one of Trackdown's placeholders — a
203
+ # missing field must be indistinguishable from nothing, never from an
204
+ # answer.
205
+ def text(result, *readers)
206
+ value = first_value(result, readers)
207
+ return nil if value.nil?
208
+
209
+ string = value.to_s.strip
210
+ return nil if string.empty?
211
+ return nil if PLACEHOLDER_VALUES.include?(string.downcase)
212
+
213
+ string
214
+ end
215
+
216
+ def number(result, *readers)
217
+ value = first_value(result, readers)
218
+ return nil if value.nil?
219
+ return nil if value.to_s.strip.empty?
220
+
221
+ value
222
+ end
223
+
224
+ def first_value(result, readers)
225
+ readers.each do |reader|
226
+ next unless result.respond_to?(reader)
227
+
228
+ value = result.public_send(reader)
229
+ return value unless value.nil?
230
+ rescue StandardError
231
+ # A reader that raises is treated as a reader the provider does not
232
+ # have. One awkward field must not cost the whole result.
233
+ next
234
+ end
235
+
236
+ nil
237
+ end
238
+
239
+ def ensure_supported_trackdown_version!
240
+ version = Gem::Version.new(::Trackdown::VERSION.to_s) if defined?(::Trackdown::VERSION)
241
+ return if version && version >= MINIMUM_TRACKDOWN_VERSION
242
+
243
+ installed = version ? version.to_s : "unknown"
244
+ raise ConfigurationError,
245
+ "Clickwrap::IpGeolocation::TrackdownResolver requires trackdown >= " \
246
+ "#{MINIMUM_TRACKDOWN_VERSION}; the loaded version is #{installed}. Run " \
247
+ "`bundle update trackdown` before enabling this resolver. Earlier releases do " \
248
+ "not expose the per-request source trust and provider provenance Clickwrap " \
249
+ "would otherwise have to guess."
250
+ rescue ArgumentError
251
+ raise ConfigurationError,
252
+ "Clickwrap::IpGeolocation::TrackdownResolver could not parse the loaded " \
253
+ "Trackdown::VERSION (#{::Trackdown::VERSION.inspect}). Install trackdown >= " \
254
+ "#{MINIMUM_TRACKDOWN_VERSION} before enabling this resolver."
255
+ end
256
+
257
+ def provider_name(result)
258
+ text(result, :provider_name, :provider) || PROVIDER_NAME
259
+ end
260
+
261
+ def provider_source(result)
262
+ text(result, :provider_source) || @provider_source_fallback
263
+ end
264
+
265
+ # Trackdown exposes MaxMind's exact database build epoch rather than a
266
+ # marketing-style version label. Preserve that integer in a
267
+ # self-describing string: formatting it as a date would throw away the
268
+ # fact that it came from the database metadata and could create timezone
269
+ # ambiguity years later.
270
+ def database_version(result)
271
+ legacy_version = text(result, :database_version, :database_build_version)
272
+ return legacy_version if legacy_version
273
+
274
+ epoch = first_value(result, %i[database_build_epoch])
275
+ return nil unless epoch.is_a?(Numeric)
276
+
277
+ "database_build_epoch:#{epoch.to_i}"
278
+ end
279
+
280
+ def database_sha256(result)
281
+ digest = text(result, :database_sha256, :database_digest)
282
+ return nil if digest.nil?
283
+ return digest if digest.match?(/\Asha256:[0-9a-f]{64}\z/i)
284
+ return "sha256:#{digest.downcase}" if digest.match?(/\A[0-9a-f]{64}\z/i)
285
+
286
+ # Do not relabel a provider's non-SHA value as SHA-256. Retaining it is
287
+ # more honest than inventing an algorithm; a host can still inspect the
288
+ # upstream value and a verifier will not mistake it for our digest form.
289
+ digest
290
+ end
291
+
292
+ def source_was_verified_by_host?(result)
293
+ result.respond_to?(:source_was_verified_by_host?) &&
294
+ result.source_was_verified_by_host? == true
295
+ rescue StandardError
296
+ false
297
+ end
298
+
299
+ def resolved_at(result)
300
+ first_value(result, %i[resolved_at]) || Clickwrap.now
301
+ end
302
+
303
+ # Fallback provenance for failures that occur before Trackdown can return
304
+ # a result. Successful Trackdown 0.4 results name the provider that
305
+ # actually answered, including when configuration.provider is `:auto`.
306
+ def configured_provider_source
307
+ return "unspecified" unless ::Trackdown.respond_to?(:configuration)
308
+
309
+ ::Trackdown.configuration.provider.to_s
310
+ rescue StandardError
311
+ "unspecified"
312
+ end
313
+
314
+ def detect_capabilities
315
+ result_class = trackdown_result_class
316
+ return PINNED_CAPABILITIES.dup if result_class.nil?
317
+
318
+ supplied = FIELD_READERS.select do |_field, readers|
319
+ readers.any? { |reader| result_class.method_defined?(reader) }
320
+ end
321
+
322
+ supplied.keys
323
+ end
324
+
325
+ def trackdown_result_class
326
+ defined?(::Trackdown::LocationResult) ? ::Trackdown::LocationResult : nil
327
+ end
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Built-in resolver value objects and adapters are part of Clickwrap's public
4
+ # initializer API, so they must be available before Rails finishes setting up
5
+ # Zeitwerk. In particular, a host should be able to write the documented line
6
+ #
7
+ # config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
8
+ #
9
+ # without knowing a private file path or adding a load-order `require`. The
10
+ # Trackdown adapter itself still loads the optional `trackdown` gem lazily in
11
+ # its constructor; requiring Clickwrap never makes Trackdown a dependency.
12
+ require_relative "ip_geolocation/location"
13
+ require_relative "ip_geolocation/resolver"
14
+ require_relative "ip_geolocation/null_resolver"
15
+ require_relative "ip_geolocation/static_resolver"
16
+ require_relative "ip_geolocation/trackdown_resolver"