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,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