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,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "cgi/escape"
5
+
6
+ module Clickwrap
7
+ # The human-readable projection of a receipt.
8
+ #
9
+ # Same facts as the canonical JSON, rendered for someone who has to read them
10
+ # — a reviewer answering a complaint, an operator investigating a dispute, a
11
+ # person asking what they agreed to. Never a different set of facts, and never
12
+ # a stronger claim: where the JSON says a digest detects ordinary modification,
13
+ # the HTML says that too, in a sentence.
14
+ #
15
+ # It is deliberately plain, self-contained HTML with no framework, no external
16
+ # asset, and no JavaScript, because a receipt frequently ends up saved to disk,
17
+ # attached to an email, or printed, and it has to still make sense there.
18
+ class ReceiptHtml
19
+ def initialize(receipt, view_context: nil, body: nil)
20
+ @receipt = receipt
21
+ @view_context = view_context
22
+ @body = body || receipt.to_h
23
+ end
24
+
25
+ attr_reader :receipt, :body
26
+
27
+ def render
28
+ ERB.new(TEMPLATE, trim_mode: "-").result(binding)
29
+ end
30
+
31
+ private
32
+
33
+ def h(value) = CGI.escapeHTML(value.to_s)
34
+
35
+ def acts = body["acts"] || []
36
+ def documents = body["documents"] || []
37
+ def presentation = body["presentation"] || {}
38
+ def integrity = body["integrity"] || {}
39
+ def retention = body["retention"] || {}
40
+ def request_evidence = body["request_evidence"] || {}
41
+ def lifecycle = body["lifecycle"] || {}
42
+ def system_info = body["system"] || {}
43
+
44
+ def kind_sentence(act)
45
+ subject = h(act["assertion"])
46
+
47
+ case act["kind"]
48
+ when "agreement" then "Agreed to: #{subject}"
49
+ when "acknowledgment" then "Acknowledged: #{subject}"
50
+ when "consent" then act["action"] == "declined" ? "Declined: #{subject}" : "Consented to: #{subject}"
51
+ when "declaration" then "Declared: #{subject}"
52
+ when "attestation" then "Attested: #{subject}"
53
+ when "authorization" then "Authorized: #{subject}"
54
+ else "#{h(act["action"])}: #{subject}"
55
+ end
56
+ end
57
+
58
+ # Each state gets a sentence rather than a bare word, because "blank" and
59
+ # "deleted" and "we never collected this" mean completely different things
60
+ # and a reader should not have to know the vocabulary to tell them apart.
61
+ def request_evidence_sentence(category, fragment)
62
+ name = category.tr("_", " ")
63
+
64
+ case fragment["state"]
65
+ when "not_configured"
66
+ "No #{name} was recorded. This policy does not collect it."
67
+ when "unavailable"
68
+ "The #{name} could not be resolved (#{h(fragment["unavailable_reason"])}), so none was recorded."
69
+ when "recorded"
70
+ "The #{name} was recorded and is shown below."
71
+ when "redacted_for_this_viewer"
72
+ "A #{name} was recorded. It is not shown to this viewer."
73
+ when "deleted_after_retention"
74
+ "The #{name} was recorded and has since been deleted under the retention policy" \
75
+ "#{" on #{h(fragment["deleted_at"])}" if fragment["deleted_at"]}."
76
+ when "held"
77
+ "A #{name} was recorded and is under a legal hold."
78
+ else
79
+ h(fragment["state"])
80
+ end
81
+ end
82
+
83
+ # The long template is kept beside the private render helpers it calls, then
84
+ # made genuinely private with `private_constant` below.
85
+ TEMPLATE = <<~HTML
86
+ <!DOCTYPE html>
87
+ <html lang="en">
88
+ <head>
89
+ <meta charset="utf-8">
90
+ <title>Clickwrap receipt <%= h(body["event_id"]) %></title>
91
+ <style>
92
+ body { font: 16px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; margin: 2rem auto;
93
+ max-width: 46rem; color: #14171a; padding: 0 1rem; }
94
+ h1 { font-size: 1.4rem; margin-bottom: 0.2rem; }
95
+ h2 { font-size: 1.05rem; margin: 2rem 0 0.5rem; border-bottom: 1px solid #d9dde1;
96
+ padding-bottom: 0.3rem; }
97
+ .subtle { color: #5b6570; font-size: 0.9rem; }
98
+ dl { display: grid; grid-template-columns: 14rem 1fr; gap: 0.35rem 1rem; margin: 0; }
99
+ dt { color: #5b6570; }
100
+ dd { margin: 0; word-break: break-word; }
101
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85em;
102
+ background: #f2f4f6; padding: 0.1em 0.35em; border-radius: 3px; }
103
+ ul { padding-left: 1.2rem; }
104
+ li { margin-bottom: 0.4rem; }
105
+ .bounded { background: #f7f8f9; border-left: 3px solid #b6bec6; padding: 0.7rem 1rem;
106
+ margin: 1rem 0; font-size: 0.92rem; color: #3c4650; }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <h1>Evidence receipt</h1>
111
+ <p class="subtle"><code><%= h(body["event_id"]) %></code> &middot;
112
+ <%= h(body["event_type"]) %> &middot;
113
+ recorded by the server at <%= h(body["recorded_at_by_server"]) %></p>
114
+
115
+ <h2>What was done</h2>
116
+ <ul>
117
+ <% acts.each do |act| %>
118
+ <li>
119
+ <%= kind_sentence(act) %>
120
+ <div class="subtle">
121
+ <%= h(act["kind"]) %> &middot; <%= h(act["action"]) %>
122
+ <% if act["expires_at"] %> &middot; valid until <%= h(act["expires_at"]) %><% end %>
123
+ <% if act["one_time"] %> &middot; single use<% end %>
124
+ </div>
125
+ </li>
126
+ <% end %>
127
+ </ul>
128
+
129
+ <h2>Exactly which documents</h2>
130
+ <ul>
131
+ <% documents.each do |document| %>
132
+ <li><%= h(document["key"]) %> version <%= h(document["version"]) %>
133
+ (<%= h(document["locale"]) %>)<br>
134
+ <span class="subtle">
135
+ Source (<%= h(document["source_media_type"]) %>):
136
+ <code><%= h(document["source_digest"]) %></code><br>
137
+ Rendered (<%= h(document["rendered_media_type"]) %>):
138
+ <code><%= h(document["rendered_digest"]) %></code>
139
+ </span></li>
140
+ <% end %>
141
+ </ul>
142
+
143
+ <h2>Who</h2>
144
+ <dl>
145
+ <dt>Actor reference</dt><dd><code><%= h(body.dig("actor", "reference")) %></code></dd>
146
+ <dt>Attribution</dt><dd><%= h(body.dig("actor", "attribution", "method")) %></dd>
147
+ <% if body.dig("actor", "tenant") %>
148
+ <dt>Tenant</dt><dd><code><%= h(body.dig("actor", "tenant")) %></code></dd>
149
+ <% end %>
150
+ <% if body.dig("actor", "subject") %>
151
+ <dt>Subject</dt><dd><code><%= h(body.dig("actor", "subject", "reference")) %></code></dd>
152
+ <% end %>
153
+ </dl>
154
+
155
+ <% unless presentation.empty? %>
156
+ <h2>What the application offered</h2>
157
+ <dl>
158
+ <dt>Call to action</dt><dd><%= h(presentation["submit_button_text"]) %></dd>
159
+ <% if presentation["combined_sentence"] %>
160
+ <dt>Sentence offered</dt><dd><%= h(presentation["combined_sentence"]) %></dd>
161
+ <dt>Covered by one control</dt><dd><%= h(Array(presentation["combined_statements"]).join(", ")) %></dd>
162
+ <% end %>
163
+ <dt>Locale</dt><dd><%= h(presentation["locale"]) %></dd>
164
+ <dt>Channel</dt><dd><%= h(presentation["capture_channel"]) %></dd>
165
+ <dt>Offered at</dt><dd><%= h(presentation["offered_at"]) %></dd>
166
+ <dt>Manifest digest</dt><dd><code><%= h(presentation["manifest_digest"]) %></code></dd>
167
+ </dl>
168
+ <p class="bounded"><%= h(presentation["proves"]) %></p>
169
+ <% end %>
170
+
171
+ <% if body["outcome"] %>
172
+ <h2>What it authorized</h2>
173
+ <dl>
174
+ <% body["outcome"].each do |key, value| %>
175
+ <dt><%= h(key.to_s.tr("_", " ")) %></dt><dd><%= h(value) %></dd>
176
+ <% end %>
177
+ </dl>
178
+ <% end %>
179
+
180
+ <h2>Request evidence</h2>
181
+ <ul>
182
+ <% request_evidence.each do |category, fragment| %>
183
+ <li><%= request_evidence_sentence(category, fragment) %>
184
+ <% if fragment["means"] %>
185
+ <div class="subtle"><%= h(fragment["means"]) %></div>
186
+ <% end %>
187
+ </li>
188
+ <% end %>
189
+ </ul>
190
+
191
+ <% if lifecycle["successors"]&.any? %>
192
+ <h2>What happened afterwards</h2>
193
+ <ul>
194
+ <% lifecycle["successors"].each do |successor| %>
195
+ <li><%= h(successor["event_type"]) %> at <%= h(successor["recorded_at_by_server"]) %>
196
+ <% if successor["reason"] %><br><span class="subtle"><%= h(successor["reason"]) %></span><% end %>
197
+ </li>
198
+ <% end %>
199
+ </ul>
200
+ <% end %>
201
+
202
+ <h2>Retention</h2>
203
+ <dl>
204
+ <dt>Retention class</dt><dd><%= h(retention["class"]) %></dd>
205
+ <% if retention["core_event_retained_until"] %>
206
+ <dt>Core event kept until</dt><dd><%= h(retention["core_event_retained_until"]) %></dd>
207
+ <% end %>
208
+ <% if retention["retention_rule"] %>
209
+ <dt>Retention rule</dt><dd><%= h(retention["retention_rule"]) %> (resolved from host events)</dd>
210
+ <% end %>
211
+ <dt>Legal hold</dt><dd><%= retention["on_legal_hold"] ? "In effect" : "None" %></dd>
212
+ </dl>
213
+
214
+ <h2>Integrity</h2>
215
+ <dl>
216
+ <dt>Digest algorithm</dt><dd><%= h(integrity["digest_algorithm"]) %></dd>
217
+ <dt>Event digest</dt><dd><code><%= h(integrity["event_digest"]) %></code></dd>
218
+ <dt>Assurance tier</dt><dd><%= h(integrity["tier"]) %></dd>
219
+ </dl>
220
+ <p class="bounded"><%= h(integrity["detects"]) %></p>
221
+
222
+ <h2>How to check this yourself</h2>
223
+ <p class="subtle"><%= h(body["verifier_instructions"]) %></p>
224
+ <dl>
225
+ <dt>Receipt schema</dt><dd><%= h(body["schema"]) %></dd>
226
+ <dt>Gem version</dt><dd><%= h(system_info["gem_version"]) %></dd>
227
+ <dt>Application version</dt><dd><%= h(system_info["application_version"]) %></dd>
228
+ <dt>Verifier version</dt><dd><%= h(system_info["verifier_version"]) %></dd>
229
+ </dl>
230
+ </body>
231
+ </html>
232
+ HTML
233
+ private_constant :TEMPLATE
234
+ end
235
+ end