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,716 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
# Builds a presentation: resolves the policy's documents and copy for one
|
|
5
|
+
# locale, records the exact call to action, and signs the result into a
|
|
6
|
+
# short-lived token.
|
|
7
|
+
#
|
|
8
|
+
# This is the one place that turns a policy into something a person can see,
|
|
9
|
+
# and everything that renders — the form-builder helper, the reference views,
|
|
10
|
+
# the engine's standalone screen, a JSON API response, a fully custom design
|
|
11
|
+
# system — goes through it. There is deliberately no second path: a shortcut
|
|
12
|
+
# that skipped the manifest would produce evidence that looks identical and
|
|
13
|
+
# proves considerably less.
|
|
14
|
+
class Presenter
|
|
15
|
+
# The only two kinds a composed sentence may absorb. A consent must stay
|
|
16
|
+
# unbundled to be separately withdrawable and separately provable, and a
|
|
17
|
+
# declaration, attestation, or authorization is a specific assertion someone
|
|
18
|
+
# should have to read on its own line rather than find folded into a
|
|
19
|
+
# sentence about something else.
|
|
20
|
+
COMPOSABLE_KINDS = %w[agreement acknowledgment].freeze
|
|
21
|
+
|
|
22
|
+
# Where a sentence fragment puts the documents it is about.
|
|
23
|
+
DOCUMENTS_PLACEHOLDER = "%{documents}"
|
|
24
|
+
|
|
25
|
+
Statement = Data.define(:key, :kind, :assertion, :label, :required, :optional, :choices,
|
|
26
|
+
:requires_an_explicit_choice, :documents, :control_name,
|
|
27
|
+
:control_id, :error_id, :purpose_key, :withdrawal_path) do
|
|
28
|
+
def required? = required
|
|
29
|
+
def optional? = optional
|
|
30
|
+
def requires_an_explicit_choice? = requires_an_explicit_choice
|
|
31
|
+
def checkbox? = choices.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Document = Data.define(:key, :label, :version_label, :locale, :source_media_type,
|
|
35
|
+
:source_content_digest, :rendered_media_type,
|
|
36
|
+
:rendered_content_digest, :renderer_name, :renderer_version,
|
|
37
|
+
:sanitizer_name, :sanitizer_version, :version_id, :path)
|
|
38
|
+
|
|
39
|
+
# One statement's share of a composed sentence, already split around the
|
|
40
|
+
# place its documents go. Kept as parts rather than a template with a
|
|
41
|
+
# placeholder so a view can drop real links into the middle of a sentence
|
|
42
|
+
# without ever concatenating HTML into translated text.
|
|
43
|
+
Fragment = Data.define(:statement_key, :kind, :prefix, :suffix, :documents, :documents_joiner) do
|
|
44
|
+
def to_text = "#{prefix}#{documents.map(&:label).join(documents_joiner)}#{suffix}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The one control and the one sentence a composable policy renders. It is
|
|
48
|
+
# not a statement and never becomes one: the statements it covers keep their
|
|
49
|
+
# own kinds, documents, answers, and lifecycles in the evidence. What this
|
|
50
|
+
# object describes is the offer a person saw.
|
|
51
|
+
#
|
|
52
|
+
# It answers everything a Statement answers about a CONTROL — the name, the
|
|
53
|
+
# id, the error id, whether it is required, whether it has choices — so the
|
|
54
|
+
# reference views render one control the same way whether it stands for one
|
|
55
|
+
# act or three, and a host's ejected statement partial styles both. What it
|
|
56
|
+
# does not answer is anything about an act, because it is not one.
|
|
57
|
+
Combined = Data.define(:sentence, :fragments, :joiner, :terminator, :statement_keys,
|
|
58
|
+
:control_name, :control_id, :error_id) do
|
|
59
|
+
def covers?(statement_key) = statement_keys.include?(statement_key.to_s)
|
|
60
|
+
|
|
61
|
+
# Always. Only required statements compose, so the single control is
|
|
62
|
+
# required too — which also lets a custom surface pass this object to
|
|
63
|
+
# `clickwrap_statement_check_box` and get the same markup contract a
|
|
64
|
+
# statement gets.
|
|
65
|
+
def required? = true
|
|
66
|
+
def optional? = false
|
|
67
|
+
def checkbox? = true
|
|
68
|
+
def choices = nil
|
|
69
|
+
def requires_an_explicit_choice? = false
|
|
70
|
+
|
|
71
|
+
# Composed, so there is no single kind — and "combined" is what the
|
|
72
|
+
# rendered class name should say, because that is what a person is
|
|
73
|
+
# looking at.
|
|
74
|
+
def kind = "combined"
|
|
75
|
+
|
|
76
|
+
# The words, without their links. The views render the linked form
|
|
77
|
+
# through `clickwrap_statement_label`; this is for anything reading the
|
|
78
|
+
# offer as text.
|
|
79
|
+
def assertion = sentence
|
|
80
|
+
|
|
81
|
+
# The links live inside the sentence, so there is no list of documents to
|
|
82
|
+
# render beneath it.
|
|
83
|
+
def documents = [].freeze
|
|
84
|
+
def withdrawal_path = nil
|
|
85
|
+
|
|
86
|
+
# The statement whose name the single control carries. Every covered
|
|
87
|
+
# statement is answered by it; this is the one whose key it is submitted
|
|
88
|
+
# under, so a browser sends one value and the server fans it out.
|
|
89
|
+
def answered_as = statement_keys.first
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Result = Data.define(:policy, :manifest, :token, :statements, :combined, :submit_button_text,
|
|
93
|
+
:locale, :actor, :subject, :represented_party, :tenant_key) do
|
|
94
|
+
def statement(key) = statements.find { |candidate| candidate.key == key.to_s }
|
|
95
|
+
def combined? = !combined.nil?
|
|
96
|
+
def policy_key = policy.key
|
|
97
|
+
def revision = manifest.revision_digest
|
|
98
|
+
def to_h = manifest.to_h
|
|
99
|
+
def as_json(*) = manifest.to_h
|
|
100
|
+
|
|
101
|
+
# The statements this presentation renders as controls of their own:
|
|
102
|
+
# everything the composed line could not honestly absorb.
|
|
103
|
+
def itemized_statements
|
|
104
|
+
return statements if combined.nil?
|
|
105
|
+
|
|
106
|
+
statements.reject { |statement| combined.covers?(statement.key) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def initialize(policy:, actor: nil, subject: nil, tenant: nil, locale: nil,
|
|
111
|
+
submit_button_text: nil, capture_channel: :web_browser,
|
|
112
|
+
registration_flow_id: nil, prospective_actor: nil, acting_for: nil,
|
|
113
|
+
represented_party_creation_flow_id: nil,
|
|
114
|
+
authentication_context: nil,
|
|
115
|
+
document_version_path_with: nil,
|
|
116
|
+
default_document_version_path_with: nil,
|
|
117
|
+
combined: true)
|
|
118
|
+
@policy = policy
|
|
119
|
+
@actor = actor
|
|
120
|
+
@prospective_actor = prospective_actor
|
|
121
|
+
@subject = subject
|
|
122
|
+
@tenant = tenant
|
|
123
|
+
@locale = (locale || default_locale).to_s
|
|
124
|
+
@submit_button_text = submit_button_text
|
|
125
|
+
@capture_channel = capture_channel.to_s
|
|
126
|
+
@registration_flow_id = registration_flow_id
|
|
127
|
+
@acting_for = acting_for
|
|
128
|
+
@represented_party_creation_flow_id = represented_party_creation_flow_id
|
|
129
|
+
@authentication_context = (authentication_context || {}).to_h.symbolize_keys
|
|
130
|
+
@document_version_path_with = document_version_path_with
|
|
131
|
+
# Wired by `form.clickwrap` and `present_clickwrap` from the render
|
|
132
|
+
# context, never by a host: it is the engine fallback with the render's
|
|
133
|
+
# own Hotwire Native treatment attached, and it is asked LAST, after a
|
|
134
|
+
# document's declared `link:` has had its say.
|
|
135
|
+
@default_document_version_path_with = default_document_version_path_with
|
|
136
|
+
@combined = combined != false
|
|
137
|
+
|
|
138
|
+
return unless @document_version_path_with && !@document_version_path_with.respond_to?(:call)
|
|
139
|
+
|
|
140
|
+
raise ArgumentError,
|
|
141
|
+
"document_version_path_with must respond to call(version), so Clickwrap can bind " \
|
|
142
|
+
"the exact immutable link target into the presentation."
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
attr_reader :policy, :actor, :subject, :tenant, :locale, :capture_channel
|
|
146
|
+
|
|
147
|
+
def present
|
|
148
|
+
policy.validate_tenant!(tenant)
|
|
149
|
+
validate_actor_binding!
|
|
150
|
+
validate_subject_binding!
|
|
151
|
+
validate_represented_party_binding!
|
|
152
|
+
validate_channel!
|
|
153
|
+
validate_locale!
|
|
154
|
+
authority_at_presentation = authority_at_presentation_snapshot
|
|
155
|
+
|
|
156
|
+
revision = PolicyRevision.freeze_for(policy)
|
|
157
|
+
resolved = policy.statements.map { |statement| resolve_statement(statement) }
|
|
158
|
+
combined = compose(resolved)
|
|
159
|
+
|
|
160
|
+
manifest = PresentationManifest.build(
|
|
161
|
+
policy: policy,
|
|
162
|
+
revision_digest: revision.revision_digest,
|
|
163
|
+
statements: resolved.map { |statement| manifest_fragment(statement) },
|
|
164
|
+
submit_button_text: @submit_button_text,
|
|
165
|
+
locale: locale,
|
|
166
|
+
actor_reference: actor_reference,
|
|
167
|
+
actor_type: actor&.class&.name,
|
|
168
|
+
tenant_key: tenant_key,
|
|
169
|
+
subject_key: subject_key,
|
|
170
|
+
subject_fingerprint: subject_fingerprint,
|
|
171
|
+
registration_flow_id: @registration_flow_id,
|
|
172
|
+
prospective_actor_type: @prospective_actor&.class&.name,
|
|
173
|
+
represented_party_reference: represented_party_reference,
|
|
174
|
+
represented_party_type: @acting_for&.class&.name,
|
|
175
|
+
authority_rule: policy.authority_rule&.to_snapshot,
|
|
176
|
+
represented_party_creation_flow_id: @represented_party_creation_flow_id,
|
|
177
|
+
represented_party_will_be_created_by_protected_action:
|
|
178
|
+
prospective_represented_party?,
|
|
179
|
+
authority_at_presentation: authority_at_presentation,
|
|
180
|
+
combined_control: combined_control_fragment(combined),
|
|
181
|
+
capture_channel: capture_channel
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
persist_presentation(manifest, revision) if policy.persist_presentations?
|
|
185
|
+
|
|
186
|
+
Result.new(
|
|
187
|
+
policy: policy,
|
|
188
|
+
manifest: manifest,
|
|
189
|
+
token: manifest.to_token,
|
|
190
|
+
statements: resolved,
|
|
191
|
+
combined: combined,
|
|
192
|
+
submit_button_text: @submit_button_text,
|
|
193
|
+
locale: locale,
|
|
194
|
+
actor: actor,
|
|
195
|
+
subject: subject,
|
|
196
|
+
represented_party: @acting_for,
|
|
197
|
+
tenant_key: tenant_key
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The fingerprint of whatever the policy says identifies this subject. It is
|
|
202
|
+
# how an authorization for one withdrawal stops being usable for a different
|
|
203
|
+
# one, and how a declaration about one set of orders stops covering a
|
|
204
|
+
# changed set.
|
|
205
|
+
def subject_fingerprint
|
|
206
|
+
SubjectFingerprint.for(policy, subject)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def subject_key = StatementState.subject_key_for(subject)
|
|
210
|
+
|
|
211
|
+
def tenant_key = Reference.tenant(tenant)
|
|
212
|
+
|
|
213
|
+
def actor_reference
|
|
214
|
+
return nil if actor.nil?
|
|
215
|
+
|
|
216
|
+
Reference.actor(actor)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def represented_party_reference
|
|
220
|
+
return nil if @acting_for.nil?
|
|
221
|
+
return "represented_party_creation/#{@represented_party_creation_flow_id}" if prospective_represented_party?
|
|
222
|
+
|
|
223
|
+
Reference.represented_party(@acting_for)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
def validate_subject_binding!
|
|
229
|
+
return unless policy.subject_bound? && subject.nil?
|
|
230
|
+
|
|
231
|
+
raise DefinitionError,
|
|
232
|
+
"Policy #{policy.key} binds evidence to a subject fingerprint, so presenting it " \
|
|
233
|
+
"requires `subject:`. A subject-bound policy cannot be completed from a generic " \
|
|
234
|
+
"URL with no server-owned resource context."
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def validate_actor_binding!
|
|
238
|
+
return if actor
|
|
239
|
+
|
|
240
|
+
unless @prospective_actor
|
|
241
|
+
raise DefinitionError,
|
|
242
|
+
"Presenting policy #{policy.key} without an actor requires `prospective_actor:`. " \
|
|
243
|
+
"Use it only for a new account registration flow; anonymous users need an " \
|
|
244
|
+
"explicit `Clickwrap.anonymous_actor(...)` reference."
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
return if @registration_flow_id.present?
|
|
248
|
+
|
|
249
|
+
raise DefinitionError,
|
|
250
|
+
"A prospective-actor presentation needs `registration_flow_id:` from server-owned " \
|
|
251
|
+
"session state, so a token from another signup flow cannot create evidence for this account."
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def validate_represented_party_binding!
|
|
255
|
+
if @acting_for.nil?
|
|
256
|
+
if @represented_party_creation_flow_id.present?
|
|
257
|
+
raise DefinitionError,
|
|
258
|
+
"A represented-party creation flow needs `acting_for:` to name the exact new record."
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# A policy may support represented-party actions and ordinary personal
|
|
262
|
+
# actions. No represented party on this particular presentation is a
|
|
263
|
+
# valid, explicitly bound state.
|
|
264
|
+
return
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
unless policy.permits_acting_for_party?(@acting_for)
|
|
268
|
+
allowed = policy.authority_rule&.represented_party_types
|
|
269
|
+
raise DefinitionError,
|
|
270
|
+
"Policy #{policy.key} does not permit acting for #{@acting_for.class.name}. " \
|
|
271
|
+
"Allowed represented-party types: #{allowed&.join(", ").presence || "(none)"}."
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
if prospective_represented_party?
|
|
275
|
+
unless policy.authority_rule.allows_represented_party_creation?
|
|
276
|
+
raise DefinitionError,
|
|
277
|
+
"Policy #{policy.key} does not permit creating the represented party inside " \
|
|
278
|
+
"the protected action. Opt in explicitly in the represented-party authority rule."
|
|
279
|
+
end
|
|
280
|
+
if @represented_party_creation_flow_id.blank?
|
|
281
|
+
raise DefinitionError,
|
|
282
|
+
"Presenting a new represented party needs " \
|
|
283
|
+
"`represented_party_creation_flow_id:` from server-owned session state."
|
|
284
|
+
end
|
|
285
|
+
elsif @represented_party_creation_flow_id.present?
|
|
286
|
+
raise DefinitionError,
|
|
287
|
+
"`represented_party_creation_flow_id:` is only valid while `acting_for:` is a new record."
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def prospective_represented_party?
|
|
292
|
+
@acting_for.respond_to?(:new_record?) && @acting_for.new_record?
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def authority_at_presentation_snapshot
|
|
296
|
+
return nil if @acting_for.nil?
|
|
297
|
+
|
|
298
|
+
if prospective_represented_party?
|
|
299
|
+
return {
|
|
300
|
+
"state" => "not_yet_verifiable",
|
|
301
|
+
"reason" => "represented_party_will_be_created_by_protected_action"
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
AuthorityVerifier.verify!(
|
|
306
|
+
policy: policy,
|
|
307
|
+
actor: actor,
|
|
308
|
+
represented_party: @acting_for,
|
|
309
|
+
tenant: tenant,
|
|
310
|
+
authentication_context: @authentication_context
|
|
311
|
+
).to_snapshot
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def default_locale
|
|
315
|
+
defined?(::I18n) ? ::I18n.locale : :en
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def validate_channel!
|
|
319
|
+
return if policy.permits_capture_channel?(capture_channel)
|
|
320
|
+
|
|
321
|
+
raise DefinitionError,
|
|
322
|
+
"Policy #{policy.key} does not accept captures from #{capture_channel}. It allows: " \
|
|
323
|
+
"#{policy.capture_channels.join(", ")}."
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def validate_locale!
|
|
327
|
+
return if policy.permits_locale?(locale)
|
|
328
|
+
|
|
329
|
+
raise MissingTranslation.new(key: policy.key, locale: locale)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def resolve_statement(statement)
|
|
333
|
+
copy = statement.resolve_copy(locale: locale)
|
|
334
|
+
documents = resolve_documents(statement)
|
|
335
|
+
|
|
336
|
+
Statement.new(
|
|
337
|
+
key: statement.key,
|
|
338
|
+
kind: statement.kind,
|
|
339
|
+
assertion: copy["assertion"],
|
|
340
|
+
label: copy["label"],
|
|
341
|
+
required: statement.required?,
|
|
342
|
+
optional: statement.optional?,
|
|
343
|
+
choices: statement.choices,
|
|
344
|
+
requires_an_explicit_choice: statement.requires_an_explicit_choice?,
|
|
345
|
+
documents: documents,
|
|
346
|
+
control_name: control_name(statement),
|
|
347
|
+
control_id: "clickwrap_#{policy.key}_#{statement.key}",
|
|
348
|
+
error_id: "clickwrap_#{policy.key}_#{statement.key}_error",
|
|
349
|
+
purpose_key: statement.purpose_key,
|
|
350
|
+
withdrawal_path: statement.withdrawal_path
|
|
351
|
+
)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# --- Composing the one-line offer ----------------------------------------
|
|
355
|
+
#
|
|
356
|
+
# A signup screen asks for two ordinary things — agree to the Terms,
|
|
357
|
+
# acknowledge the Privacy Notice — and rendering them as two stacked
|
|
358
|
+
# checkboxes with a "Required" flag, a version label, and an "(opens in a
|
|
359
|
+
# new tab)" hint apiece is four times the interface the moment deserves.
|
|
360
|
+
# When every statement is one of those ordinary things, they compose into
|
|
361
|
+
# ONE control carrying ONE sentence with the documents linked inside it.
|
|
362
|
+
#
|
|
363
|
+
# Composing is a decision about PRESENTATION only. The evidence keeps every
|
|
364
|
+
# statement separate — its own kind, its own documents, its own answer, its
|
|
365
|
+
# own lifecycle — because "agreed to the Terms" and "acknowledged the
|
|
366
|
+
# Privacy Notice" remain different facts no matter how few boxes it took to
|
|
367
|
+
# say both.
|
|
368
|
+
|
|
369
|
+
def compose(resolved)
|
|
370
|
+
return nil unless @combined
|
|
371
|
+
|
|
372
|
+
composable = resolved.select { |statement| composable?(statement) }
|
|
373
|
+
return nil if composable.empty?
|
|
374
|
+
|
|
375
|
+
connectives = sentence_connectives
|
|
376
|
+
return nil if connectives.nil?
|
|
377
|
+
|
|
378
|
+
fragments = composable.map { |statement| sentence_fragment(statement, connectives) }
|
|
379
|
+
return nil if fragments.any?(&:nil?)
|
|
380
|
+
|
|
381
|
+
build_combined(composable, fragments, connectives)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def build_combined(composable, fragments, connectives)
|
|
385
|
+
control = composable.first
|
|
386
|
+
|
|
387
|
+
Combined.new(
|
|
388
|
+
sentence: fragments.map(&:to_text).join(connectives[:joiner]) + connectives[:terminator],
|
|
389
|
+
fragments: fragments.freeze,
|
|
390
|
+
joiner: connectives[:joiner],
|
|
391
|
+
terminator: connectives[:terminator],
|
|
392
|
+
statement_keys: composable.map(&:key).freeze,
|
|
393
|
+
control_name: control.control_name,
|
|
394
|
+
control_id: control.control_id,
|
|
395
|
+
error_id: control.error_id
|
|
396
|
+
)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Every clause here is a way a statement can be more than the sentence can
|
|
400
|
+
# honestly say. An optional consent bundled into a required line would make
|
|
401
|
+
# it required; a choice folded into a checkbox would turn a recorded "no"
|
|
402
|
+
# into an unrecorded silence; a statement with a withdrawal route needs that
|
|
403
|
+
# route beside the control it belongs to; and copy the application wrote
|
|
404
|
+
# itself is copy it wrote for a reason.
|
|
405
|
+
def composable?(statement)
|
|
406
|
+
return false unless COMPOSABLE_KINDS.include?(statement.kind)
|
|
407
|
+
return false unless statement.required? && statement.checkbox?
|
|
408
|
+
return false if statement.withdrawal_path.present?
|
|
409
|
+
return false if statement.documents.empty?
|
|
410
|
+
|
|
411
|
+
default_worded?(policy.statement!(statement.key))
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# The assertion is still the conventional key the DSL fills in, so nobody
|
|
415
|
+
# has chosen these words for this policy. A policy that did — a literal, a
|
|
416
|
+
# locale map, its own I18n key — gets its own control and its own sentence,
|
|
417
|
+
# unedited.
|
|
418
|
+
def default_worded?(declared)
|
|
419
|
+
declared.assertion.declaration == :"clickwrap.statements.#{declared.kind}.#{declared.key}"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# The words that hold a composed sentence together, in the locale being
|
|
423
|
+
# presented. A locale that has not translated them composes nothing and
|
|
424
|
+
# renders itemized instead — half a sentence in the wrong language is worse
|
|
425
|
+
# than two tidy lines in the right one.
|
|
426
|
+
def sentence_connectives
|
|
427
|
+
joiner = sentence_text("joiner")
|
|
428
|
+
documents_joiner = sentence_text("documents_joiner")
|
|
429
|
+
terminator = sentence_text("terminator")
|
|
430
|
+
return nil if joiner.nil? || documents_joiner.nil? || terminator.nil?
|
|
431
|
+
|
|
432
|
+
{ joiner: joiner, documents_joiner: documents_joiner, terminator: terminator }
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def sentence_fragment(statement, connectives)
|
|
436
|
+
template = sentence_text(statement.kind)
|
|
437
|
+
return nil if template.nil?
|
|
438
|
+
|
|
439
|
+
prefix, suffix = template.split(DOCUMENTS_PLACEHOLDER, 2)
|
|
440
|
+
# A translation with nowhere to put the documents would render a sentence
|
|
441
|
+
# about documents nobody can open. Itemize instead.
|
|
442
|
+
return nil if suffix.nil?
|
|
443
|
+
|
|
444
|
+
Fragment.new(
|
|
445
|
+
statement_key: statement.key,
|
|
446
|
+
kind: statement.kind,
|
|
447
|
+
prefix: prefix,
|
|
448
|
+
suffix: suffix,
|
|
449
|
+
documents: statement.documents,
|
|
450
|
+
documents_joiner: connectives[:documents_joiner]
|
|
451
|
+
)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# What the manifest signs about a composed offer. The per-statement
|
|
455
|
+
# fragments already carry the acts, the documents, and their digests; this
|
|
456
|
+
# carries the thing only the composed shape has — the exact sentence a
|
|
457
|
+
# person read, and which statement keys the one answer they gave covers.
|
|
458
|
+
def combined_control_fragment(combined)
|
|
459
|
+
return nil if combined.nil?
|
|
460
|
+
|
|
461
|
+
{
|
|
462
|
+
"sentence" => combined.sentence,
|
|
463
|
+
"covers" => combined.statement_keys,
|
|
464
|
+
"answered_as" => combined.answered_as,
|
|
465
|
+
"control_name" => combined.control_name
|
|
466
|
+
}
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def sentence_text(key)
|
|
470
|
+
return nil unless defined?(::I18n)
|
|
471
|
+
|
|
472
|
+
text = ::I18n.t("clickwrap.sentence.#{key}", locale: locale, default: nil)
|
|
473
|
+
text&.to_s
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# The form field name. Answers arrive nested under one key so a host's
|
|
477
|
+
# strong parameters can permit the whole envelope in one line, and so an
|
|
478
|
+
# answer can never be mistaken for one of the host's own attributes.
|
|
479
|
+
def control_name(statement)
|
|
480
|
+
"clickwrap_submission[answers][#{statement.key}]"
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def resolve_documents(statement)
|
|
484
|
+
statement.document_keys.filter_map do |document_key|
|
|
485
|
+
version = current_document_version(document_key)
|
|
486
|
+
|
|
487
|
+
unless version
|
|
488
|
+
raise DocumentNotPublishedError,
|
|
489
|
+
"Policy #{policy.key} presents #{document_key} but no published version of it " \
|
|
490
|
+
"is effective for locale #{locale}. Declare it with `Clickwrap.document " \
|
|
491
|
+
"#{document_key.to_sym.inspect}, version: \"...\", from: ...` and run " \
|
|
492
|
+
"`bin/rails clickwrap:publish`."
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
label, = statement.link_labels[document_key]&.resolve(locale: locale)
|
|
496
|
+
|
|
497
|
+
Document.new(
|
|
498
|
+
key: document_key,
|
|
499
|
+
label: label || document_key.humanize,
|
|
500
|
+
version_label: version.version_label,
|
|
501
|
+
locale: version.locale,
|
|
502
|
+
source_media_type: version.media_type,
|
|
503
|
+
source_content_digest: version.content_digest,
|
|
504
|
+
rendered_media_type: version.rendered_media_type.presence || version.media_type,
|
|
505
|
+
rendered_content_digest: version.rendered_content_digest.presence || version.content_digest,
|
|
506
|
+
renderer_name: version.renderer_name,
|
|
507
|
+
renderer_version: version.renderer_version,
|
|
508
|
+
sanitizer_name: version.sanitizer_name,
|
|
509
|
+
sanitizer_version: version.sanitizer_version,
|
|
510
|
+
version_id: version.id,
|
|
511
|
+
path: document_path(version, declared_link_for(document_key, version))
|
|
512
|
+
)
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# The `link:` the application declared for exactly these bytes: same tenant,
|
|
517
|
+
# same key, same version label, same locale. Read from the registry rather
|
|
518
|
+
# than the database because it is a presentation decision, not evidence —
|
|
519
|
+
# the evidence is the digest of the bytes, which the row already holds.
|
|
520
|
+
def declared_link_for(document_key, version)
|
|
521
|
+
document = documents_for_this_policy[document_key.to_s]
|
|
522
|
+
return nil unless document
|
|
523
|
+
|
|
524
|
+
Clickwrap.documents[[document.tenant_key, document.document_key,
|
|
525
|
+
version.version_label, version.locale]]&.link
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Documents are immutable and published: within one presentation build, the
|
|
529
|
+
# answer for a key cannot change, and two statements naming the same
|
|
530
|
+
# document must get the same answer anyway. So every key this policy
|
|
531
|
+
# references is resolved once, in two queries, rather than one lookup (or
|
|
532
|
+
# two) plus a version query per statement-document pair.
|
|
533
|
+
def current_document_version(document_key)
|
|
534
|
+
current_document_versions[document_key.to_s]
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def current_document_versions
|
|
538
|
+
@current_document_versions ||= begin
|
|
539
|
+
documents = documents_for_this_policy
|
|
540
|
+
versions = newest_effective_versions_by_document_id(documents.values.map(&:id))
|
|
541
|
+
|
|
542
|
+
documents.transform_values { |document| versions[document.id] }
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# The order here is the whole contract of a document link.
|
|
547
|
+
#
|
|
548
|
+
# 1. A resolver the CALLER passed wins outright: it is the host saying, at
|
|
549
|
+
# this call site, exactly where this document lives.
|
|
550
|
+
# 2. The document's declared `link:` — the host's own reader-facing page.
|
|
551
|
+
# 3. The render context's default resolver: the mounted engine route, with
|
|
552
|
+
# whatever Hotwire Native treatment this request calls for. It is given
|
|
553
|
+
# the declared link too, so a native render absolutizes a host page the
|
|
554
|
+
# same way it absolutizes an engine path.
|
|
555
|
+
# 4. Nothing but the engine's own routes, which refuse rather than sign a
|
|
556
|
+
# path that resolves to nothing.
|
|
557
|
+
def document_path(version, link)
|
|
558
|
+
path =
|
|
559
|
+
if @document_version_path_with
|
|
560
|
+
@document_version_path_with.call(version)
|
|
561
|
+
elsif @default_document_version_path_with
|
|
562
|
+
@default_document_version_path_with.call(version, link)
|
|
563
|
+
elsif link.present?
|
|
564
|
+
link
|
|
565
|
+
elsif defined?(Clickwrap::Engine)
|
|
566
|
+
engine_document_version_path(version)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
return path.to_s if path.present?
|
|
570
|
+
|
|
571
|
+
raise ConfigurationError,
|
|
572
|
+
"Clickwrap could not build the immutable URL for document version #{version.id}. " \
|
|
573
|
+
"Present through form.clickwrap or present_clickwrap so the mounted route is known, " \
|
|
574
|
+
"or pass document_version_path_with: ->(version) { ... }."
|
|
575
|
+
rescue Clickwrap::Error
|
|
576
|
+
raise
|
|
577
|
+
rescue StandardError => error
|
|
578
|
+
raise ConfigurationError,
|
|
579
|
+
"Clickwrap could not build the immutable URL for document version #{version.id}: " \
|
|
580
|
+
"#{error.class}: #{error.message}"
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# The tenant's own document wins over the shared one, exactly as the
|
|
584
|
+
# per-key lookup did. When there is no tenant the two scopes are the same
|
|
585
|
+
# query, so only one is issued.
|
|
586
|
+
def documents_for_this_policy
|
|
587
|
+
@documents_for_this_policy ||= begin
|
|
588
|
+
keys = policy.statements.flat_map(&:document_keys).uniq
|
|
589
|
+
|
|
590
|
+
if keys.empty?
|
|
591
|
+
{}
|
|
592
|
+
else
|
|
593
|
+
shared = ::Clickwrap::Document.where(document_key: keys, tenant_key: nil).index_by(&:document_key)
|
|
594
|
+
|
|
595
|
+
if tenant_key.blank?
|
|
596
|
+
shared
|
|
597
|
+
else
|
|
598
|
+
shared.merge(::Clickwrap::Document.for_tenant(tenant_key)
|
|
599
|
+
.where(document_key: keys).index_by(&:document_key))
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# One query for every version this presentation could offer, ordered the
|
|
606
|
+
# same way Document#current_version orders one document's versions, so the
|
|
607
|
+
# first row per document is the one that method would have returned.
|
|
608
|
+
def newest_effective_versions_by_document_id(document_ids)
|
|
609
|
+
return {} if document_ids.empty?
|
|
610
|
+
|
|
611
|
+
at = Clickwrap.now
|
|
612
|
+
|
|
613
|
+
::Clickwrap::DocumentVersion
|
|
614
|
+
.published
|
|
615
|
+
.effective_at_or_before(at)
|
|
616
|
+
.not_retired_at(at)
|
|
617
|
+
.for_locale(locale)
|
|
618
|
+
.where(document_id: document_ids)
|
|
619
|
+
.order(effective_at: :desc, published_at: :desc, created_at: :desc)
|
|
620
|
+
.group_by(&:document_id)
|
|
621
|
+
.transform_values(&:first)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# The engine's own URL helpers carry no mount prefix, so on an application
|
|
625
|
+
# that never mounted the engine they answer with a path that resolves to
|
|
626
|
+
# nothing. That answer would be *signed*: it goes into the manifest, into
|
|
627
|
+
# the digest, and into the evidence as the exact document the person was
|
|
628
|
+
# offered. A signed dead link is the worst failure this gem has — it looks
|
|
629
|
+
# like evidence and cites a 404 — so it is refused at build time instead.
|
|
630
|
+
def engine_document_version_path(version)
|
|
631
|
+
ControllerHelpers.assert_engine_can_resolve_document_links!(version)
|
|
632
|
+
|
|
633
|
+
Clickwrap::Engine.routes.url_helpers.document_version_path(version.id)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def manifest_fragment(statement)
|
|
637
|
+
declared = policy.statement!(statement.key)
|
|
638
|
+
|
|
639
|
+
{
|
|
640
|
+
"key" => statement.key,
|
|
641
|
+
"kind" => statement.kind,
|
|
642
|
+
"assertion" => statement.assertion,
|
|
643
|
+
"label" => statement.label,
|
|
644
|
+
"required" => statement.required?,
|
|
645
|
+
"optional" => statement.optional?,
|
|
646
|
+
"choices" => statement.choices,
|
|
647
|
+
"requires_an_explicit_choice" => statement.requires_an_explicit_choice?,
|
|
648
|
+
"purpose" => statement.purpose_key,
|
|
649
|
+
"withdrawal_path" => statement.withdrawal_path,
|
|
650
|
+
"valid_for_seconds" => declared.valid_for&.to_i,
|
|
651
|
+
"one_time" => declared.one_time?,
|
|
652
|
+
"requires" => declared.requires,
|
|
653
|
+
"requires_current_version" => declared.requires_current_version?,
|
|
654
|
+
"subject_fingerprint" => subject_fingerprint_for(declared),
|
|
655
|
+
"subject_fingerprint_version" => declared.subject_fingerprint_version,
|
|
656
|
+
"protected_outcome_version" => declared.protected_outcome_version,
|
|
657
|
+
"control_name" => statement.control_name,
|
|
658
|
+
"documents" => statement.documents.map do |document|
|
|
659
|
+
{
|
|
660
|
+
"key" => document.key,
|
|
661
|
+
"label" => document.label,
|
|
662
|
+
"version" => document.version_label,
|
|
663
|
+
"locale" => document.locale,
|
|
664
|
+
"source_media_type" => document.source_media_type,
|
|
665
|
+
"source_digest" => document.source_content_digest,
|
|
666
|
+
"rendered_media_type" => document.rendered_media_type,
|
|
667
|
+
"rendered_digest" => document.rendered_content_digest,
|
|
668
|
+
"renderer" => {
|
|
669
|
+
"name" => document.renderer_name,
|
|
670
|
+
"version" => document.renderer_version,
|
|
671
|
+
"sanitizer_name" => document.sanitizer_name,
|
|
672
|
+
"sanitizer_version" => document.sanitizer_version
|
|
673
|
+
}.compact.presence,
|
|
674
|
+
"version_id" => document.version_id.to_s,
|
|
675
|
+
"path" => document.path
|
|
676
|
+
}
|
|
677
|
+
end
|
|
678
|
+
}.compact
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def subject_fingerprint_for(statement)
|
|
682
|
+
return nil unless statement.subject_bound?
|
|
683
|
+
|
|
684
|
+
SubjectFingerprint.for_statement(statement, subject)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Only written when a policy asked for it. The state is `presented_by_server`
|
|
688
|
+
# and nothing stronger: the server generated and offered this. Whether a
|
|
689
|
+
# human ever saw it is not something a web server can know.
|
|
690
|
+
def persist_presentation(manifest, revision)
|
|
691
|
+
Presentation.create!(
|
|
692
|
+
policy_key: policy.key,
|
|
693
|
+
policy_revision: revision,
|
|
694
|
+
nonce: manifest.nonce,
|
|
695
|
+
manifest: manifest.to_h,
|
|
696
|
+
manifest_digest: manifest.digest,
|
|
697
|
+
actor: actor.is_a?(::ActiveRecord::Base) ? actor : nil,
|
|
698
|
+
actor_reference: actor_reference,
|
|
699
|
+
registration_flow_id: @registration_flow_id,
|
|
700
|
+
represented_party_type: @acting_for.is_a?(::ActiveRecord::Base) ? @acting_for.class.name : nil,
|
|
701
|
+
represented_party_id: @acting_for.is_a?(::ActiveRecord::Base) ? @acting_for.id : nil,
|
|
702
|
+
represented_party_reference: represented_party_reference.presence,
|
|
703
|
+
tenant_key: tenant_key,
|
|
704
|
+
subject: subject.is_a?(::ActiveRecord::Base) ? subject : nil,
|
|
705
|
+
subject_fingerprint: subject_fingerprint,
|
|
706
|
+
locale: locale,
|
|
707
|
+
capture_channel: capture_channel,
|
|
708
|
+
state: "presented_by_server",
|
|
709
|
+
issued_at: manifest.issued_at,
|
|
710
|
+
expires_at: manifest.expires_at,
|
|
711
|
+
retain_until: Clickwrap.now + policy.persist_presentations_for,
|
|
712
|
+
created_at: Clickwrap.now
|
|
713
|
+
)
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
end
|