clickwrap 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +90 -0
- data/CHANGELOG.md +612 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +241 -0
- data/app/controllers/clickwrap/application_controller.rb +79 -0
- data/app/controllers/clickwrap/captures_controller.rb +145 -0
- data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
- data/app/controllers/clickwrap/receipts_controller.rb +115 -0
- data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
- data/app/helpers/clickwrap/engine_helper.rb +97 -0
- data/app/views/clickwrap/captures/show.html.erb +34 -0
- data/app/views/clickwrap/receipts/index.html.erb +38 -0
- data/app/views/clickwrap/receipts/show.html.erb +91 -0
- data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
- data/app/views/clickwrap/shared/_fields.html.erb +100 -0
- data/app/views/clickwrap/shared/_statement.html.erb +105 -0
- data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
- data/config/locales/en.yml +160 -0
- data/config/locales/es.yml +138 -0
- data/config/routes.rb +41 -0
- data/exe/clickwrap +374 -0
- data/guides/README.md +30 -0
- data/guides/accessibility.md +249 -0
- data/guides/consent-and-lifecycle.md +299 -0
- data/guides/integrating.md +610 -0
- data/guides/integrity.md +212 -0
- data/guides/migrating.md +335 -0
- data/guides/naming.md +320 -0
- data/guides/organizations.md +320 -0
- data/guides/receipts-and-verification.md +415 -0
- data/guides/request-evidence.md +512 -0
- data/guides/retention-and-legal-holds.md +438 -0
- data/lib/clickwrap/actor_proxy.rb +147 -0
- data/lib/clickwrap/anonymous_actor.rb +47 -0
- data/lib/clickwrap/authority.rb +174 -0
- data/lib/clickwrap/canonical_json.rb +216 -0
- data/lib/clickwrap/capture/event_builder.rb +220 -0
- data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
- data/lib/clickwrap/capture.rb +650 -0
- data/lib/clickwrap/configuration.rb +1129 -0
- data/lib/clickwrap/controller_helpers.rb +758 -0
- data/lib/clickwrap/current_state.rb +282 -0
- data/lib/clickwrap/digest.rb +125 -0
- data/lib/clickwrap/doctor.rb +418 -0
- data/lib/clickwrap/document_definition.rb +255 -0
- data/lib/clickwrap/document_renderer.rb +83 -0
- data/lib/clickwrap/document_renderers/markdown.rb +175 -0
- data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
- data/lib/clickwrap/dsl/policy_builder.rb +462 -0
- data/lib/clickwrap/dsl/retention_builder.rb +89 -0
- data/lib/clickwrap/durable_commit_callback.rb +37 -0
- data/lib/clickwrap/engine.rb +184 -0
- data/lib/clickwrap/errors.rb +181 -0
- data/lib/clickwrap/form_builder_extensions.rb +341 -0
- data/lib/clickwrap/front_matter.rb +67 -0
- data/lib/clickwrap/identifier.rb +112 -0
- data/lib/clickwrap/import/external_receipt.rb +241 -0
- data/lib/clickwrap/import/fine_print.rb +290 -0
- data/lib/clickwrap/import/legacy.rb +450 -0
- data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
- data/lib/clickwrap/integrity/anchor.rb +130 -0
- data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
- data/lib/clickwrap/integrity/attestor.rb +221 -0
- data/lib/clickwrap/integrity/chain.rb +313 -0
- data/lib/clickwrap/integrity/timestamp.rb +143 -0
- data/lib/clickwrap/ip_geolocation/location.rb +112 -0
- data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
- data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
- data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
- data/lib/clickwrap/ip_geolocation.rb +16 -0
- data/lib/clickwrap/lifecycle.rb +534 -0
- data/lib/clickwrap/linter.rb +382 -0
- data/lib/clickwrap/localized_text.rb +101 -0
- data/lib/clickwrap/macros.rb +203 -0
- data/lib/clickwrap/models/application_record.rb +20 -0
- data/lib/clickwrap/models/chain_head.rb +79 -0
- data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
- data/lib/clickwrap/models/disposition_plan.rb +208 -0
- data/lib/clickwrap/models/document.rb +46 -0
- data/lib/clickwrap/models/document_version.rb +163 -0
- data/lib/clickwrap/models/event.rb +743 -0
- data/lib/clickwrap/models/event_document.rb +79 -0
- data/lib/clickwrap/models/event_statement.rb +92 -0
- data/lib/clickwrap/models/external_action.rb +150 -0
- data/lib/clickwrap/models/integrity_attestation.rb +90 -0
- data/lib/clickwrap/models/legal_hold.rb +81 -0
- data/lib/clickwrap/models/policy_revision.rb +115 -0
- data/lib/clickwrap/models/presentation.rb +59 -0
- data/lib/clickwrap/models/receipt_access.rb +53 -0
- data/lib/clickwrap/models/recording_sequence.rb +21 -0
- data/lib/clickwrap/models/request_evidence.rb +378 -0
- data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
- data/lib/clickwrap/models/statement_state.rb +130 -0
- data/lib/clickwrap/pending_receipt.rb +177 -0
- data/lib/clickwrap/policy.rb +283 -0
- data/lib/clickwrap/presentation_manifest.rb +210 -0
- data/lib/clickwrap/presenter.rb +716 -0
- data/lib/clickwrap/privacy.rb +419 -0
- data/lib/clickwrap/protected_outcome.rb +120 -0
- data/lib/clickwrap/receipt.rb +606 -0
- data/lib/clickwrap/receipt_html.rb +235 -0
- data/lib/clickwrap/receipt_verifier.rb +978 -0
- data/lib/clickwrap/reference.rb +44 -0
- data/lib/clickwrap/registration.rb +236 -0
- data/lib/clickwrap/registry.rb +54 -0
- data/lib/clickwrap/remediation_token.rb +155 -0
- data/lib/clickwrap/request_evidence_extractor.rb +590 -0
- data/lib/clickwrap/request_evidence_policy.rb +261 -0
- data/lib/clickwrap/retention/applier.rb +231 -0
- data/lib/clickwrap/retention/disposition.rb +221 -0
- data/lib/clickwrap/retention/planner.rb +502 -0
- data/lib/clickwrap/retention_class.rb +97 -0
- data/lib/clickwrap/reviewed_text.rb +28 -0
- data/lib/clickwrap/schema_requirements.rb +196 -0
- data/lib/clickwrap/services/authorize_external_action.rb +149 -0
- data/lib/clickwrap/services/load_policies.rb +69 -0
- data/lib/clickwrap/services/publish_documents.rb +251 -0
- data/lib/clickwrap/services/validate_policy_references.rb +166 -0
- data/lib/clickwrap/statement.rb +248 -0
- data/lib/clickwrap/subject_fingerprint.rb +28 -0
- data/lib/clickwrap/submission.rb +169 -0
- data/lib/clickwrap/system_actor.rb +31 -0
- data/lib/clickwrap/test_helpers.rb +676 -0
- data/lib/clickwrap/testing.rb +211 -0
- data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
- data/lib/clickwrap/verification.rb +504 -0
- data/lib/clickwrap/version.rb +12 -1
- data/lib/clickwrap/view_helpers.rb +190 -0
- data/lib/clickwrap/vocabulary.rb +294 -0
- data/lib/clickwrap.rb +497 -7
- data/lib/generators/clickwrap/document_generator.rb +164 -0
- data/lib/generators/clickwrap/hardening_generator.rb +177 -0
- data/lib/generators/clickwrap/install_generator.rb +1287 -0
- data/lib/generators/clickwrap/link_generator.rb +56 -0
- data/lib/generators/clickwrap/policy_generator.rb +118 -0
- data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
- data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
- data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
- data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
- data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
- data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
- data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
- data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
- data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
- data/lib/generators/clickwrap/views_generator.rb +101 -0
- data/lib/tasks/clickwrap.rake +569 -0
- metadata +311 -16
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
# View helpers for CUSTOM presentation surfaces — the middle ground between
|
|
5
|
+
# `form.clickwrap` (the one-line happy path) and hand-writing every input
|
|
6
|
+
# against `Clickwrap.present` primitives.
|
|
7
|
+
#
|
|
8
|
+
# A custom surface has to render three things correctly or its captures
|
|
9
|
+
# silently fail at submit: the signed token under the exact envelope name,
|
|
10
|
+
# each control under its statement's declared name and id, and a call to
|
|
11
|
+
# action whose words match the ones the manifest recorded. These helpers own
|
|
12
|
+
# those three contracts; the host owns every class, wrapper, and data
|
|
13
|
+
# attribute around them.
|
|
14
|
+
#
|
|
15
|
+
# <% preparation = Clickwrap.present(:withdrawal_preparation, actor: current_user,
|
|
16
|
+
# submit_button_text: "He leído todo") %>
|
|
17
|
+
# <%= clickwrap_presentation_token_field(preparation) %>
|
|
18
|
+
# <% preparation.statements.each do |statement| %>
|
|
19
|
+
# <%= clickwrap_statement_check_box(statement, class: "my-checkbox") %>
|
|
20
|
+
# <%= label_tag statement.control_id, statement.assertion %>
|
|
21
|
+
# <% end %>
|
|
22
|
+
# <%= clickwrap_submit_button(preparation, class: "my-button") %>
|
|
23
|
+
module ViewHelpers
|
|
24
|
+
# Host-specific navigation attributes for immutable document links. This
|
|
25
|
+
# helper deliberately lives with the helpers installed into every host
|
|
26
|
+
# view, rather than only in EngineHelper: `form.clickwrap` renders the
|
|
27
|
+
# engine's statement partial inside the host form's view context.
|
|
28
|
+
#
|
|
29
|
+
# The immutable href is evidence-critical and therefore cannot be
|
|
30
|
+
# overridden here. Hosts may only choose how their client opens it (for
|
|
31
|
+
# example, target: "_blank" or data: { turbo: false }).
|
|
32
|
+
#
|
|
33
|
+
# The hook is evaluated IN the rendering view, so a host can answer
|
|
34
|
+
# per-request questions the way it always does — `hotwire_native_app?`
|
|
35
|
+
# being the canonical one: a native WebView usually wants a same-window
|
|
36
|
+
# link its screen rules can route, while the web wants a new tab.
|
|
37
|
+
#
|
|
38
|
+
# `config.hotwire_native_document_links`, when set, answers native renders
|
|
39
|
+
# entirely (the attributes here, and the absolutized href in
|
|
40
|
+
# ControllerHelpers#clickwrap_document_version_path_for_presentation);
|
|
41
|
+
# the hook keeps answering everything else.
|
|
42
|
+
def clickwrap_document_link_html_options(document = nil)
|
|
43
|
+
native_options = clickwrap_hotwire_native_link_options
|
|
44
|
+
return native_options if native_options
|
|
45
|
+
|
|
46
|
+
raw_options = instance_exec(document, &Clickwrap.config.document_link_html_options_with)
|
|
47
|
+
unless raw_options.is_a?(Hash) || raw_options.is_a?(ActiveSupport::HashWithIndifferentAccess)
|
|
48
|
+
raise ConfigurationError,
|
|
49
|
+
"document_link_html_options_with must return a Hash of HTML attributes."
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
options = raw_options.to_h.symbolize_keys
|
|
53
|
+
# Case-insensitive on purpose: HTML lowercases attribute names and keeps
|
|
54
|
+
# the FIRST duplicate, so a smuggled "HREF" would win over the signed
|
|
55
|
+
# immutable path in the rendered link.
|
|
56
|
+
if options.keys.any? { |key| key.to_s.casecmp?("href") }
|
|
57
|
+
raise ConfigurationError,
|
|
58
|
+
"document_link_html_options_with cannot set href. Clickwrap signs the exact " \
|
|
59
|
+
"immutable document path into the presentation manifest; this hook may only " \
|
|
60
|
+
"choose how the client opens that path."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
options
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The declared native answer, or nil when this render is not one of the
|
|
67
|
+
# cases `config.hotwire_native_document_links` decides.
|
|
68
|
+
def clickwrap_hotwire_native_link_options
|
|
69
|
+
native_links = Clickwrap.config.hotwire_native_document_links
|
|
70
|
+
return nil unless native_links
|
|
71
|
+
return nil unless respond_to?(:hotwire_native_app?) && hotwire_native_app?
|
|
72
|
+
|
|
73
|
+
# Asked with the CONTROLLER, not the view: the href was resolved from the
|
|
74
|
+
# controller when the presentation was signed, and the two halves of one
|
|
75
|
+
# document link must not be able to answer differently.
|
|
76
|
+
case Clickwrap.config.hotwire_native_document_link_mode(clickwrap_native_link_context)
|
|
77
|
+
when :external_browser
|
|
78
|
+
# `data-turbo-false` keeps the tap out of the Turbo/Hotwire Native
|
|
79
|
+
# navigation stack so the absolutized href reaches the system browser.
|
|
80
|
+
{ target: "_blank", rel: "noopener", data: { turbo: false } }
|
|
81
|
+
when :same_screen
|
|
82
|
+
# A plain link, on purpose: the app's own native path configuration
|
|
83
|
+
# decides how the document presents (typically a modal sheet).
|
|
84
|
+
{}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def clickwrap_native_link_context
|
|
89
|
+
(controller if respond_to?(:controller)) || self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The words that go in one control's label: a statement's own first-person
|
|
93
|
+
# assertion, or — when the control is the composed line — the whole
|
|
94
|
+
# sentence with its document links rendered inside it.
|
|
95
|
+
#
|
|
96
|
+
# One helper for both is what lets the reference statement partial render
|
|
97
|
+
# either without knowing which it has, and what makes a host's ejected
|
|
98
|
+
# partial style the composed line for free.
|
|
99
|
+
def clickwrap_statement_label(control)
|
|
100
|
+
return clickwrap_combined_sentence(control) if control.is_a?(Clickwrap::Presenter::Combined)
|
|
101
|
+
|
|
102
|
+
control.assertion
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The composed one-line offer, as markup: the sentence the presentation
|
|
106
|
+
# signed, with each document rendered as a real link where the words for it
|
|
107
|
+
# go. This is what the single checkbox's label contains, and it is a helper
|
|
108
|
+
# rather than template soup because an ejected view should be able to
|
|
109
|
+
# restyle the line without reassembling a sentence out of translated parts.
|
|
110
|
+
#
|
|
111
|
+
# <%= label_tag presentation.combined.control_id,
|
|
112
|
+
# clickwrap_combined_sentence(presentation.combined) %>
|
|
113
|
+
#
|
|
114
|
+
# Nothing here interpolates markup into translated text: the presenter
|
|
115
|
+
# already split every fragment around the place its documents go, so this
|
|
116
|
+
# only joins pieces that are individually safe.
|
|
117
|
+
def clickwrap_combined_sentence(combined)
|
|
118
|
+
fragments = combined.fragments.map { |fragment| clickwrap_sentence_fragment(fragment) }
|
|
119
|
+
|
|
120
|
+
safe_join([safe_join(fragments, combined.joiner), combined.terminator])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# One statement's share of that sentence.
|
|
124
|
+
def clickwrap_sentence_fragment(fragment)
|
|
125
|
+
links = fragment.documents.map { |document| clickwrap_document_link(document) }
|
|
126
|
+
|
|
127
|
+
safe_join([fragment.prefix, safe_join(links, fragment.documents_joiner), fragment.suffix])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# One document, as the link a person presses to read it. The href is the
|
|
131
|
+
# exact path signed into the manifest and cannot be overridden here; the
|
|
132
|
+
# host's navigation hook chooses only how its client opens it. There is no
|
|
133
|
+
# "unavailable document" branch, because there is no such document: a
|
|
134
|
+
# presentation whose path could not be resolved refuses to be built at all.
|
|
135
|
+
#
|
|
136
|
+
# The "(opens in a new tab)" truth is kept and the clutter is not: it is
|
|
137
|
+
# rendered for screen readers only, and only when the link really does open
|
|
138
|
+
# a new tab — a same-window link announcing otherwise would be the page
|
|
139
|
+
# lying about itself.
|
|
140
|
+
def clickwrap_document_link(document)
|
|
141
|
+
options = clickwrap_document_link_html_options(document)
|
|
142
|
+
link = link_to(document.label, document.path, class: "clickwrap-documents__link", **options)
|
|
143
|
+
return link unless options[:target].to_s == "_blank"
|
|
144
|
+
|
|
145
|
+
# The separating space lives INSIDE the sr-only span, which is out of
|
|
146
|
+
# flow: a space between the elements would be laid out, and the sentence
|
|
147
|
+
# would read "…Privacy Policy ." with a gap before its own full stop.
|
|
148
|
+
hint = tag.span(" #{t("clickwrap.ui.opens_in_new_tab")}", class: "clickwrap-sr-only")
|
|
149
|
+
safe_join([link, hint])
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The signed presentation token, under the envelope name the capture reads.
|
|
153
|
+
# This is the one hidden field a clickwrap form carries — and the one whose
|
|
154
|
+
# name must never be hand-typed, because a typo here is a form that looks
|
|
155
|
+
# complete and refuses every submission.
|
|
156
|
+
def clickwrap_presentation_token_field(presentation)
|
|
157
|
+
hidden_field_tag "clickwrap_submission[presentation_token]", presentation.token, id: nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# One statement's checkbox: declared name and id, initially unchecked
|
|
161
|
+
# (always — a pre-ticked box records the page's default, not the person's
|
|
162
|
+
# action), `required` mirroring the server's own rule as progressive
|
|
163
|
+
# enhancement. Everything in `**options` is yours; pass `checked: true`
|
|
164
|
+
# only when re-rendering a submission the person already made.
|
|
165
|
+
def clickwrap_statement_check_box(statement, checked: false, **options)
|
|
166
|
+
options = { id: statement.control_id }.merge(options)
|
|
167
|
+
options[:required] = true if statement.required? && !options.key?(:required)
|
|
168
|
+
|
|
169
|
+
check_box_tag statement.control_name, "1", checked, options
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# One option of a statement rendered as a radio group — the pattern for an
|
|
173
|
+
# answer with more than one presented choice (or an affirmative/negative
|
|
174
|
+
# pair, where the negative submits "0"). All options share the statement's
|
|
175
|
+
# control name; each gets a value-suffixed id for its label.
|
|
176
|
+
def clickwrap_statement_radio_button(statement, value, checked: false, **options)
|
|
177
|
+
options = { id: "#{statement.control_id}_#{value}" }.merge(options)
|
|
178
|
+
|
|
179
|
+
radio_button_tag statement.control_name, value, checked, options
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The call to action, worded by the presentation itself. The manifest
|
|
183
|
+
# recorded `submit_button_text` when the offer was signed; rendering the
|
|
184
|
+
# button from the same object is what makes drift between the recorded
|
|
185
|
+
# words and the pressed words impossible on a custom surface.
|
|
186
|
+
def clickwrap_submit_button(presentation, **)
|
|
187
|
+
submit_tag(presentation.submit_button_text, **)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
# Every stable string Clickwrap writes into evidence lives here, frozen, in
|
|
5
|
+
# one place. Historical receipts are read by code that may be years newer
|
|
6
|
+
# than the code that wrote them, so these values are part of the public
|
|
7
|
+
# compatibility promise: they are added to, never renamed or repurposed.
|
|
8
|
+
#
|
|
9
|
+
# This taxonomy is product design. It is not a statutory vocabulary, and
|
|
10
|
+
# naming an act `agreement` or `consent` does not determine its legal effect.
|
|
11
|
+
module Vocabulary
|
|
12
|
+
# The six kinds, each with the lifecycle it actually needs.
|
|
13
|
+
KINDS = %w[
|
|
14
|
+
agreement
|
|
15
|
+
acknowledgment
|
|
16
|
+
consent
|
|
17
|
+
declaration
|
|
18
|
+
attestation
|
|
19
|
+
authorization
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
# The policy DSL verb that declares each kind.
|
|
23
|
+
VERB_FOR_KIND = {
|
|
24
|
+
"agreement" => "agree_to",
|
|
25
|
+
"acknowledgment" => "acknowledge",
|
|
26
|
+
"consent" => "consent_to",
|
|
27
|
+
"declaration" => "declare",
|
|
28
|
+
"attestation" => "attest",
|
|
29
|
+
"authorization" => "authorize"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
KIND_FOR_VERB = VERB_FOR_KIND.invert.freeze
|
|
33
|
+
|
|
34
|
+
# The action recorded when a statement is first captured.
|
|
35
|
+
INITIAL_ACTION_FOR_KIND = {
|
|
36
|
+
"agreement" => "agreed",
|
|
37
|
+
"acknowledgment" => "acknowledged",
|
|
38
|
+
"consent" => "granted",
|
|
39
|
+
"declaration" => "declared",
|
|
40
|
+
"attestation" => "attested",
|
|
41
|
+
"authorization" => "authorized"
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# Every action a statement of each kind may ever record.
|
|
45
|
+
ACTIONS_FOR_KIND = {
|
|
46
|
+
"agreement" => %w[agreed superseded].freeze,
|
|
47
|
+
"acknowledgment" => %w[acknowledged superseded expired].freeze,
|
|
48
|
+
"consent" => %w[granted declined withdrawn renewed scope_changed].freeze,
|
|
49
|
+
"declaration" => %w[declared corrected superseded expired].freeze,
|
|
50
|
+
"attestation" => %w[attested corrected superseded].freeze,
|
|
51
|
+
"authorization" => %w[authorized consumed expired revoked].freeze
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
ACTIONS = ACTIONS_FOR_KIND.values.flatten.uniq.freeze
|
|
55
|
+
|
|
56
|
+
# Kinds whose meaning includes a withdrawal route. Withdrawing future
|
|
57
|
+
# consent never rewrites a historical agreement or factual declaration, so
|
|
58
|
+
# only `consent` is withdrawable.
|
|
59
|
+
WITHDRAWABLE_KINDS = %w[consent].freeze
|
|
60
|
+
|
|
61
|
+
# Kinds that may carry a validity period.
|
|
62
|
+
EXPIRABLE_KINDS = %w[acknowledgment consent declaration authorization].freeze
|
|
63
|
+
|
|
64
|
+
# Kinds that may be corrected by the same actor without implying the
|
|
65
|
+
# original statement was false when it was made.
|
|
66
|
+
CORRECTABLE_KINDS = %w[declaration attestation].freeze
|
|
67
|
+
|
|
68
|
+
# Kinds that may be scoped to a single protected action.
|
|
69
|
+
ONE_TIME_KINDS = %w[authorization].freeze
|
|
70
|
+
|
|
71
|
+
# What produced an event. `capture` is a human action recorded by this
|
|
72
|
+
# application; every other value says plainly that something else happened.
|
|
73
|
+
EVENT_TYPES = %w[
|
|
74
|
+
capture
|
|
75
|
+
withdrawal
|
|
76
|
+
correction
|
|
77
|
+
supersession
|
|
78
|
+
expiry
|
|
79
|
+
consumption
|
|
80
|
+
revocation
|
|
81
|
+
renewal
|
|
82
|
+
scope_change
|
|
83
|
+
exemption
|
|
84
|
+
imported_legacy
|
|
85
|
+
external_receipt
|
|
86
|
+
disposition
|
|
87
|
+
legal_hold_placed
|
|
88
|
+
legal_hold_released
|
|
89
|
+
receipt_access
|
|
90
|
+
provider_outcome
|
|
91
|
+
].freeze
|
|
92
|
+
|
|
93
|
+
# Event types that record an act by a human through a Clickwrap
|
|
94
|
+
# presentation. These satisfy `agreed_to?`, `consented_to?` and the other
|
|
95
|
+
# human-action predicates — as does `imported_legacy`, which carries what a
|
|
96
|
+
# previous system recorded about a human action (an exemption never does:
|
|
97
|
+
# it records that no human acted).
|
|
98
|
+
HUMAN_ACTION_EVENT_TYPES = %w[capture correction renewal scope_change].freeze
|
|
99
|
+
|
|
100
|
+
# What belongs in an actor's own receipts collection: every act they
|
|
101
|
+
# performed here, plus their history imported from a previous system or
|
|
102
|
+
# recorded by an external provider. A migrated user's records screen must
|
|
103
|
+
# show their history, not an empty list that implies they never agreed to
|
|
104
|
+
# anything.
|
|
105
|
+
ACTOR_RECEIPT_EVENT_TYPES = (HUMAN_ACTION_EVENT_TYPES + %w[imported_legacy external_receipt]).freeze
|
|
106
|
+
|
|
107
|
+
# Where a capture came from. This is recorded, never guessed: a missing
|
|
108
|
+
# browser parameter is not evidence of a system actor.
|
|
109
|
+
CAPTURE_CHANNELS = %w[
|
|
110
|
+
web_browser
|
|
111
|
+
native_app
|
|
112
|
+
api_client
|
|
113
|
+
operator
|
|
114
|
+
background_job
|
|
115
|
+
imported_provider
|
|
116
|
+
system
|
|
117
|
+
].freeze
|
|
118
|
+
|
|
119
|
+
# The current state of one statement for one actor/subject, projected from
|
|
120
|
+
# retained event payloads.
|
|
121
|
+
STATES = %w[
|
|
122
|
+
active
|
|
123
|
+
declined
|
|
124
|
+
withdrawn
|
|
125
|
+
expired
|
|
126
|
+
superseded
|
|
127
|
+
consumed
|
|
128
|
+
revoked
|
|
129
|
+
corrected
|
|
130
|
+
exempted
|
|
131
|
+
].freeze
|
|
132
|
+
|
|
133
|
+
# A statement in one of these states cannot satisfy a requirement.
|
|
134
|
+
INACTIVE_STATES = %w[declined withdrawn expired superseded consumed revoked corrected].freeze
|
|
135
|
+
|
|
136
|
+
# How a receipt describes each optional request-evidence field. "Blank" is
|
|
137
|
+
# never allowed to blur "we chose not to collect this" into "collection
|
|
138
|
+
# failed" or "we deleted it".
|
|
139
|
+
REQUEST_EVIDENCE_STATES = %w[
|
|
140
|
+
not_configured
|
|
141
|
+
unavailable
|
|
142
|
+
recorded
|
|
143
|
+
redacted_for_this_viewer
|
|
144
|
+
deleted_after_retention
|
|
145
|
+
held
|
|
146
|
+
].freeze
|
|
147
|
+
|
|
148
|
+
# Every IP-geolocation data field a policy can enable individually.
|
|
149
|
+
# `latitude_and_longitude` is one coupled choice on purpose: half a
|
|
150
|
+
# coordinate is not a result.
|
|
151
|
+
IP_GEOLOCATION_DATA_FIELDS = %w[
|
|
152
|
+
country
|
|
153
|
+
region
|
|
154
|
+
city
|
|
155
|
+
postal_code
|
|
156
|
+
latitude_and_longitude
|
|
157
|
+
timezone
|
|
158
|
+
continent
|
|
159
|
+
metro_code
|
|
160
|
+
accuracy_radius_in_kilometers
|
|
161
|
+
].freeze
|
|
162
|
+
|
|
163
|
+
# Provenance that travels with any stored IP-geolocation result. A policy
|
|
164
|
+
# cannot keep provider-derived coordinates while stripping the uncertainty
|
|
165
|
+
# needed to interpret them.
|
|
166
|
+
IP_GEOLOCATION_PROVENANCE_FIELDS = %w[
|
|
167
|
+
ip_geolocation_provider_name
|
|
168
|
+
ip_geolocation_provider_source
|
|
169
|
+
ip_geolocation_database_version
|
|
170
|
+
ip_geolocation_database_sha256
|
|
171
|
+
ip_geolocation_accuracy_radius_confidence_percentage
|
|
172
|
+
ip_geolocation_was_estimated
|
|
173
|
+
ip_geolocation_source_was_verified_by_host
|
|
174
|
+
ip_geolocation_resolved_at
|
|
175
|
+
ip_geolocation_unavailable_reason
|
|
176
|
+
].freeze
|
|
177
|
+
|
|
178
|
+
# How the actor was attributed to the event. None of these is an identity
|
|
179
|
+
# claim; they say which application-supplied context was recorded.
|
|
180
|
+
ATTRIBUTION_METHODS = %w[
|
|
181
|
+
authenticated_session
|
|
182
|
+
account_registration
|
|
183
|
+
public_form
|
|
184
|
+
operator_session
|
|
185
|
+
api_credential
|
|
186
|
+
anonymous_identifier
|
|
187
|
+
system_process
|
|
188
|
+
imported_provider
|
|
189
|
+
unknown
|
|
190
|
+
].freeze
|
|
191
|
+
|
|
192
|
+
# Stable machine-readable reasons a verification can fail. Applications
|
|
193
|
+
# branch on these symbols; the human message is localized separately.
|
|
194
|
+
VERIFICATION_ERRORS = %i[
|
|
195
|
+
no_evidence
|
|
196
|
+
wrong_actor
|
|
197
|
+
wrong_tenant
|
|
198
|
+
wrong_subject
|
|
199
|
+
subject_fingerprint_mismatch
|
|
200
|
+
stale_policy_revision
|
|
201
|
+
unseen_document_version
|
|
202
|
+
missing_answer
|
|
203
|
+
declined
|
|
204
|
+
declaration_expired
|
|
205
|
+
acknowledgment_expired
|
|
206
|
+
consent_withdrawn
|
|
207
|
+
superseded
|
|
208
|
+
revoked
|
|
209
|
+
authorization_consumed
|
|
210
|
+
authorization_expired
|
|
211
|
+
predecessor_missing
|
|
212
|
+
wrong_order
|
|
213
|
+
replay_rejected
|
|
214
|
+
presentation_expired
|
|
215
|
+
presentation_invalid
|
|
216
|
+
presentation_actor_mismatch
|
|
217
|
+
presentation_subject_mismatch
|
|
218
|
+
presentation_tenant_mismatch
|
|
219
|
+
presentation_channel_mismatch
|
|
220
|
+
presentation_policy_mismatch
|
|
221
|
+
represented_party_mismatch
|
|
222
|
+
represented_party_authority_mismatch
|
|
223
|
+
represented_party_creation_flow_mismatch
|
|
224
|
+
registration_flow_mismatch
|
|
225
|
+
registration_actor_type_mismatch
|
|
226
|
+
document_digest_mismatch
|
|
227
|
+
integrity_check_failed
|
|
228
|
+
exemption_not_accepted
|
|
229
|
+
request_evidence_unavailable
|
|
230
|
+
core_event_disposed
|
|
231
|
+
unknown_policy
|
|
232
|
+
unknown_statement
|
|
233
|
+
].freeze
|
|
234
|
+
|
|
235
|
+
# Which assurance tier a verification result was produced under. Each tier
|
|
236
|
+
# states exactly what it detects and nothing more.
|
|
237
|
+
INTEGRITY_TIERS = %w[
|
|
238
|
+
baseline
|
|
239
|
+
database_hardening
|
|
240
|
+
chained_history
|
|
241
|
+
external_event_anchoring
|
|
242
|
+
third_party_timestamp
|
|
243
|
+
].freeze
|
|
244
|
+
|
|
245
|
+
# Public words that would overclaim what any of this proves. The release
|
|
246
|
+
# test greps generated output, receipts, task output, and documentation for
|
|
247
|
+
# these.
|
|
248
|
+
PROHIBITED_CLAIM_PHRASES = [
|
|
249
|
+
"gdpr compliant",
|
|
250
|
+
"gdpr-compliant",
|
|
251
|
+
"legally compliant",
|
|
252
|
+
"compliance guaranteed",
|
|
253
|
+
"court proof",
|
|
254
|
+
"court-proof",
|
|
255
|
+
"tamper proof",
|
|
256
|
+
"tamper-proof",
|
|
257
|
+
"legally binding",
|
|
258
|
+
"guarantees enforceability",
|
|
259
|
+
"audit guaranteed",
|
|
260
|
+
"qualified electronic signature",
|
|
261
|
+
"trusted time",
|
|
262
|
+
"trusted_timestamp",
|
|
263
|
+
"verified identity",
|
|
264
|
+
"legal advice"
|
|
265
|
+
].freeze
|
|
266
|
+
|
|
267
|
+
class << self
|
|
268
|
+
def kind?(value) = KINDS.include?(value.to_s)
|
|
269
|
+
def action?(value) = ACTIONS.include?(value.to_s)
|
|
270
|
+
def event_type?(value) = EVENT_TYPES.include?(value.to_s)
|
|
271
|
+
def capture_channel?(value) = CAPTURE_CHANNELS.include?(value.to_s)
|
|
272
|
+
def state?(value) = STATES.include?(value.to_s)
|
|
273
|
+
|
|
274
|
+
def actions_for(kind)
|
|
275
|
+
ACTIONS_FOR_KIND.fetch(kind.to_s) do
|
|
276
|
+
raise UnknownStatementError, "#{kind.inspect} is not one of: #{KINDS.join(", ")}"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def initial_action_for(kind)
|
|
281
|
+
INITIAL_ACTION_FOR_KIND.fetch(kind.to_s) do
|
|
282
|
+
raise UnknownStatementError, "#{kind.inspect} is not one of: #{KINDS.join(", ")}"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def withdrawable?(kind) = WITHDRAWABLE_KINDS.include?(kind.to_s)
|
|
287
|
+
def expirable?(kind) = EXPIRABLE_KINDS.include?(kind.to_s)
|
|
288
|
+
def correctable?(kind) = CORRECTABLE_KINDS.include?(kind.to_s)
|
|
289
|
+
def one_time_allowed?(kind) = ONE_TIME_KINDS.include?(kind.to_s)
|
|
290
|
+
def human_action_event_type?(value) = HUMAN_ACTION_EVENT_TYPES.include?(value.to_s)
|
|
291
|
+
def inactive_state?(value) = INACTIVE_STATES.include?(value.to_s)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|