clickwrap 0.0.0 → 0.1.1
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 +650 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +245 -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 +154 -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 +187 -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 +729 -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
data/lib/clickwrap.rb
CHANGED
|
@@ -1,15 +1,505 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/core_ext/string/inflections"
|
|
5
|
+
require "active_support/core_ext/numeric/time"
|
|
6
|
+
require "active_support/core_ext/object/blank"
|
|
7
|
+
|
|
3
8
|
require_relative "clickwrap/version"
|
|
9
|
+
require_relative "clickwrap/errors"
|
|
10
|
+
require_relative "clickwrap/vocabulary"
|
|
11
|
+
require_relative "clickwrap/canonical_json"
|
|
12
|
+
require_relative "clickwrap/digest"
|
|
13
|
+
require_relative "clickwrap/identifier"
|
|
14
|
+
require_relative "clickwrap/reference"
|
|
15
|
+
require_relative "clickwrap/protected_outcome"
|
|
16
|
+
require_relative "clickwrap/trusted_proxy_configuration"
|
|
17
|
+
require_relative "clickwrap/reviewed_text"
|
|
18
|
+
require_relative "clickwrap/ip_geolocation"
|
|
19
|
+
require_relative "clickwrap/authority"
|
|
20
|
+
require_relative "clickwrap/integrations/organizations_authority"
|
|
21
|
+
require_relative "clickwrap/subject_fingerprint"
|
|
22
|
+
require_relative "clickwrap/remediation_token"
|
|
23
|
+
require_relative "clickwrap/durable_commit_callback"
|
|
24
|
+
require_relative "clickwrap/front_matter"
|
|
25
|
+
require_relative "clickwrap/document_renderer"
|
|
26
|
+
require_relative "clickwrap/document_renderers/markdown"
|
|
27
|
+
require_relative "clickwrap/document_renderers/markdown_rails"
|
|
28
|
+
require_relative "clickwrap/configuration"
|
|
29
|
+
require_relative "clickwrap/macros"
|
|
30
|
+
|
|
31
|
+
require_relative "clickwrap/engine" if defined?(Rails::Engine)
|
|
4
32
|
|
|
5
|
-
#
|
|
33
|
+
# == Clickwrap
|
|
34
|
+
#
|
|
35
|
+
# The evidence-and-assent layer for Rails: versioned agreements,
|
|
36
|
+
# acknowledgments, consent, declarations, attestations, and authorizations,
|
|
37
|
+
# captured with the exact content and presentation they were offered under, and
|
|
38
|
+
# committed in the same transaction as the action they authorize.
|
|
39
|
+
#
|
|
40
|
+
# The public surface is intentionally small:
|
|
41
|
+
#
|
|
42
|
+
# Clickwrap.configure { |config| ... } # one block, in an initializer
|
|
43
|
+
# Clickwrap.document :terms, ... # immutable versioned content
|
|
44
|
+
# Clickwrap.policy :signup do ... end # what the server offers and accepts
|
|
45
|
+
# Clickwrap.retention :ordinary do ... # how long each part is kept
|
|
6
46
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# and it deliberately declares no runtime dependencies. Requiring it does
|
|
10
|
-
# nothing beyond defining this module and VERSION.
|
|
47
|
+
# has_clickwraps # on the model that can act
|
|
48
|
+
# form.clickwrap :signup, submit: "..." # render the controls and the action
|
|
11
49
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
50
|
+
# Clickwrap.capture!(:signup, actor:, http_request:, submission:)
|
|
51
|
+
# Clickwrap.capture_and!(:withdrawal, ...) { withdrawal.submit! }
|
|
52
|
+
# user.clickwraps.agreed_to?(:terms)
|
|
53
|
+
# Clickwrap.verify(:withdrawal, actor:, subject:)
|
|
54
|
+
#
|
|
55
|
+
# What this gem does is evidence mechanics. What it does not do is decide
|
|
56
|
+
# whether your agreement is enforceable, whether consent is the right lawful
|
|
57
|
+
# basis, whether a document change is material, who someone really is, or how
|
|
58
|
+
# long you must keep anything. Those belong to the application and its counsel,
|
|
59
|
+
# and no configuration flag here can stand in for them.
|
|
14
60
|
module Clickwrap
|
|
61
|
+
DOCUMENT_OPTIONS = %i[
|
|
62
|
+
version locale media_type effective_at tenant from content resolver renderer link
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
# The Minitest helpers hosts include in their own suite. Autoloaded rather
|
|
66
|
+
# than required at boot: a test helper has no business being resident in a
|
|
67
|
+
# production process, and a host that never writes a Clickwrap test never
|
|
68
|
+
# loads the file.
|
|
69
|
+
#
|
|
70
|
+
# class ActiveSupport::TestCase
|
|
71
|
+
# include Clickwrap::TestHelpers
|
|
72
|
+
# end
|
|
73
|
+
autoload :TestHelpers, "clickwrap/test_helpers"
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
# --- Configuration --------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def config
|
|
79
|
+
@config ||= Configuration.new
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
alias configuration config
|
|
83
|
+
|
|
84
|
+
def configure
|
|
85
|
+
yield config if block_given?
|
|
86
|
+
config.validate!
|
|
87
|
+
config
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Reset all global state. Used by the test suite to keep examples isolated;
|
|
91
|
+
# also handy in a console when experimenting with configuration.
|
|
92
|
+
def reset!
|
|
93
|
+
@config = Configuration.new
|
|
94
|
+
@documents = nil
|
|
95
|
+
@policies = nil
|
|
96
|
+
@retention_classes = nil
|
|
97
|
+
# Every memoized verifier goes with the configuration it was built from.
|
|
98
|
+
# A verifier that outlived a reset keeps signing and accepting tokens
|
|
99
|
+
# under the previous secret, which is the kind of thing a test suite
|
|
100
|
+
# papers over (by resetting it itself) and a console session discovers
|
|
101
|
+
# the hard way.
|
|
102
|
+
RemediationToken.reset_verifier! if defined?(RemediationToken)
|
|
103
|
+
PresentationManifest.reset_verifier! if defined?(PresentationManifest)
|
|
104
|
+
SchemaRequirements.reset! if defined?(SchemaRequirements)
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# --- Registries -----------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def documents = @documents ||= Registry.new(:document)
|
|
111
|
+
def policies = @policies ||= Registry.new(:policy)
|
|
112
|
+
def retention_classes = @retention_classes ||= Registry.new(:retention_class)
|
|
113
|
+
|
|
114
|
+
# Declares one immutable document version. Declaring it does not publish it:
|
|
115
|
+
# `bin/rails clickwrap:publish` reads the bytes once, digests them, and
|
|
116
|
+
# freezes a database snapshot. Until then the declaration is a promise about
|
|
117
|
+
# what will be published, and a policy that references an unpublished
|
|
118
|
+
# document fails loudly rather than presenting nothing.
|
|
119
|
+
def document(key, **options)
|
|
120
|
+
unknown = options.keys.map(&:to_sym) - DOCUMENT_OPTIONS
|
|
121
|
+
unless unknown.empty?
|
|
122
|
+
raise DefinitionError,
|
|
123
|
+
"Document #{key.inspect} has unknown option#{"s" if unknown.many?} " \
|
|
124
|
+
"#{unknown.map { |option| "`#{option}:`" }.join(", ")}. Supported options are: " \
|
|
125
|
+
"#{DOCUMENT_OPTIONS.map { |option| "`#{option}:`" }.join(", ")}. Clickwrap never " \
|
|
126
|
+
"ignores document options."
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
definition = DocumentDefinition.new(key: key, **options)
|
|
130
|
+
documents.register(definition.identity, definition)
|
|
131
|
+
definition
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Declares a server-owned policy. Compiles immediately so a mistake is a
|
|
135
|
+
# boot failure with a sentence explaining it, not a surprise at 3am.
|
|
136
|
+
def policy(key, &block)
|
|
137
|
+
raise DefinitionError, "Clickwrap.policy needs a block" unless block
|
|
138
|
+
|
|
139
|
+
builder = DSL::PolicyBuilder.new(key)
|
|
140
|
+
builder.instance_eval(&block)
|
|
141
|
+
compiled = builder.compile
|
|
142
|
+
|
|
143
|
+
policies.register(compiled.key, compiled)
|
|
144
|
+
compiled
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Declares a retention class. Clickwrap does not choose retention periods
|
|
148
|
+
# and cannot tell you whether yours are right; it makes a reviewed decision
|
|
149
|
+
# executable, auditable, and separable — the core event's schedule is
|
|
150
|
+
# independent of the optional personal request evidence attached to it.
|
|
151
|
+
def retention(key, &block)
|
|
152
|
+
raise DefinitionError, "Clickwrap.retention needs a block" unless block
|
|
153
|
+
|
|
154
|
+
builder = DSL::RetentionBuilder.new(key)
|
|
155
|
+
builder.instance_eval(&block)
|
|
156
|
+
compiled = builder.compile
|
|
157
|
+
|
|
158
|
+
retention_classes.register(compiled.key, compiled)
|
|
159
|
+
compiled
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def policy!(key)
|
|
163
|
+
policies.fetch(key.to_s) do
|
|
164
|
+
raise UnknownPolicyError,
|
|
165
|
+
"No policy named #{key.inspect}. Defined policies: " \
|
|
166
|
+
"#{policies.keys.sort.join(", ").presence || "(none)"}. Policies are declared with " \
|
|
167
|
+
"`Clickwrap.policy #{key.inspect} do ... end`, conventionally in config/clickwrap.rb."
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def retention_class!(key)
|
|
172
|
+
retention_classes.fetch(key.to_s) do
|
|
173
|
+
raise UnknownRetentionClassError,
|
|
174
|
+
"No retention class named #{key.inspect}. Defined classes: " \
|
|
175
|
+
"#{retention_classes.keys.sort.join(", ").presence || "(none)"}."
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def document_definitions_for(key, tenant: nil)
|
|
180
|
+
documents.values.select do |definition|
|
|
181
|
+
definition.key == key.to_s && definition.tenant_key == tenant&.to_s
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# --- Presentation and capture --------------------------------------------
|
|
186
|
+
|
|
187
|
+
def present(policy_key, **)
|
|
188
|
+
Presenter.new(policy: policy!(policy_key), **).present
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def capture!(policy_key, actor:, subject: nil, tenant: nil, http_request: nil,
|
|
192
|
+
submission: nil, answers: nil, locale: nil, capture_channel: nil,
|
|
193
|
+
acting_for: nil, authentication_context: nil, attribution_method: nil,
|
|
194
|
+
idempotency_key: nil)
|
|
195
|
+
Capture.new(
|
|
196
|
+
policy: policy!(policy_key), actor: actor, subject: subject, tenant: tenant,
|
|
197
|
+
http_request: http_request, submission: submission, answers: answers, locale: locale,
|
|
198
|
+
capture_channel: capture_channel, acting_for: acting_for,
|
|
199
|
+
authentication_context: authentication_context, attribution_method: attribution_method,
|
|
200
|
+
idempotency_key: idempotency_key
|
|
201
|
+
).capture!
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def capture_and!(policy_key, actor:, subject: nil, tenant: nil, http_request: nil,
|
|
205
|
+
submission: nil, answers: nil, locale: nil, capture_channel: nil,
|
|
206
|
+
acting_for: nil, authentication_context: nil, attribution_method: nil,
|
|
207
|
+
idempotency_key: nil, &)
|
|
208
|
+
Capture.new(
|
|
209
|
+
policy: policy!(policy_key), actor: actor, subject: subject, tenant: tenant,
|
|
210
|
+
http_request: http_request, submission: submission, answers: answers, locale: locale,
|
|
211
|
+
capture_channel: capture_channel, acting_for: acting_for,
|
|
212
|
+
authentication_context: authentication_context, attribution_method: attribution_method,
|
|
213
|
+
idempotency_key: idempotency_key
|
|
214
|
+
).capture_and!(&)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# A prospective represented-party flow for records such as a new customer
|
|
218
|
+
# organization. The form binds the record type and a server-owned browser
|
|
219
|
+
# flow before the record exists. This block must return a persisted record
|
|
220
|
+
# of that presented type after creating its host authority relationship;
|
|
221
|
+
# Clickwrap then verifies authority,
|
|
222
|
+
# rebinds the final stable reference, and commits all of it together.
|
|
223
|
+
def create_represented_party!(policy_key, actor:, represented_party:,
|
|
224
|
+
represented_party_creation_flow_id:,
|
|
225
|
+
subject: nil, tenant: nil, http_request: nil,
|
|
226
|
+
submission: nil, answers: nil, locale: nil,
|
|
227
|
+
capture_channel: nil, authentication_context: nil,
|
|
228
|
+
attribution_method: nil, idempotency_key: nil, &)
|
|
229
|
+
Capture.new(
|
|
230
|
+
policy: policy!(policy_key), actor: actor, subject: subject, tenant: tenant,
|
|
231
|
+
http_request: http_request, submission: submission, answers: answers, locale: locale,
|
|
232
|
+
capture_channel: capture_channel, acting_for: represented_party,
|
|
233
|
+
authentication_context: authentication_context, attribution_method: attribution_method,
|
|
234
|
+
idempotency_key: idempotency_key,
|
|
235
|
+
represented_party_creation_flow_id: represented_party_creation_flow_id
|
|
236
|
+
).create_represented_party!(&)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Signup, modeled honestly: at first render there is no persisted actor, so
|
|
240
|
+
# the presentation binds to a short-lived registration flow, and the account
|
|
241
|
+
# and its evidence commit together. The receipt records account-registration
|
|
242
|
+
# attribution rather than pretending someone was already authenticated.
|
|
243
|
+
#
|
|
244
|
+
# A prospective actor must be new. A public form's typed email address is
|
|
245
|
+
# not proof that its visitor controls an existing account or lead row. Use
|
|
246
|
+
# a distinct pending-request record, confirm the email address, and only
|
|
247
|
+
# then capture for the verified actor through the ordinary authenticated
|
|
248
|
+
# path.
|
|
249
|
+
def register!(policy_key, prospective_actor:, subject: nil, tenant: nil, http_request: nil,
|
|
250
|
+
submission: nil, answers: nil, locale: nil, capture_channel: nil,
|
|
251
|
+
acting_for: nil, authentication_context: nil, idempotency_key: nil,
|
|
252
|
+
registration_flow_id: nil, &)
|
|
253
|
+
Capture.new(
|
|
254
|
+
policy: policy!(policy_key), actor: nil, prospective_actor: prospective_actor,
|
|
255
|
+
subject: subject, tenant: tenant, http_request: http_request, submission: submission,
|
|
256
|
+
answers: answers, locale: locale, capture_channel: capture_channel,
|
|
257
|
+
acting_for: acting_for, authentication_context: authentication_context,
|
|
258
|
+
idempotency_key: idempotency_key, registration_flow_id: registration_flow_id
|
|
259
|
+
).register!(&)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Produces the strict, canonical result snapshot consumed by
|
|
263
|
+
# `record_protected_outcome_with`. Keeping this construction in the gem
|
|
264
|
+
# prevents every host from inventing a subtly different hash contract.
|
|
265
|
+
def protected_outcome(...) = ProtectedOutcome.build(...)
|
|
266
|
+
|
|
267
|
+
# Digest the effective proxy rules rather than a prose description of
|
|
268
|
+
# them. The Rails-specific helper includes Rails' actual defaults when the
|
|
269
|
+
# application has not overridden `action_dispatch.trusted_proxies`.
|
|
270
|
+
def trusted_proxy_configuration_digest_for(trusted_proxies)
|
|
271
|
+
TrustedProxyConfiguration.digest_for(trusted_proxies)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def trusted_proxy_configuration_digest_for_rails_application(application = Rails.application)
|
|
275
|
+
TrustedProxyConfiguration.digest_for_rails_application(application)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def submission_from(params, ...) = Submission.from_params(params, ...)
|
|
279
|
+
|
|
280
|
+
# --- Lifecycle ------------------------------------------------------------
|
|
281
|
+
#
|
|
282
|
+
# Each of these spells out the keywords its target accepts rather than
|
|
283
|
+
# forwarding `**`. A bare forward compiles fine and reads fine, and then an
|
|
284
|
+
# editor shows `**` where the argument list should be, a typo'd keyword
|
|
285
|
+
# travels one method further before failing, and the public API of the gem
|
|
286
|
+
# is documented only in the private method behind it.
|
|
287
|
+
|
|
288
|
+
def withdraw!(purpose_key, actor:, because:, tenant: nil, subject: nil,
|
|
289
|
+
acting_for: nil, http_request: nil)
|
|
290
|
+
Lifecycle.withdraw!(purpose_key, actor: actor, because: because, tenant: tenant,
|
|
291
|
+
subject: subject, acting_for: acting_for,
|
|
292
|
+
http_request: http_request)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Correcting, renewing, and rescoping are new statements by the same
|
|
296
|
+
# person, so each takes the same `submission:` a first capture does: they
|
|
297
|
+
# are captured through a real presentation, not flipped administratively.
|
|
298
|
+
def correct_declaration!(statement_key, actor:, because:, subject: nil, tenant: nil,
|
|
299
|
+
replaces: nil, acting_for: nil, http_request: nil,
|
|
300
|
+
submission: nil, answers: nil)
|
|
301
|
+
Lifecycle.correct!(statement_key, actor: actor, because: because, subject: subject,
|
|
302
|
+
tenant: tenant, replaces: replaces, acting_for: acting_for,
|
|
303
|
+
http_request: http_request, submission: submission,
|
|
304
|
+
answers: answers)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def renew!(statement_key, actor:, because:, subject: nil, tenant: nil,
|
|
308
|
+
acting_for: nil, http_request: nil, submission: nil, answers: nil)
|
|
309
|
+
Lifecycle.renew!(statement_key, actor: actor, because: because, subject: subject,
|
|
310
|
+
tenant: tenant, acting_for: acting_for,
|
|
311
|
+
http_request: http_request, submission: submission,
|
|
312
|
+
answers: answers)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def change_consent_scope!(statement_key, actor:, because:, subject: nil, tenant: nil,
|
|
316
|
+
acting_for: nil, http_request: nil, submission: nil, answers: nil)
|
|
317
|
+
Lifecycle.change_consent_scope!(statement_key, actor: actor, because: because,
|
|
318
|
+
subject: subject, tenant: tenant,
|
|
319
|
+
acting_for: acting_for,
|
|
320
|
+
http_request: http_request,
|
|
321
|
+
submission: submission, answers: answers)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def revoke!(statement_key, actor:, because:, subject: nil, tenant: nil,
|
|
325
|
+
acting_for: nil, http_request: nil)
|
|
326
|
+
Lifecycle.revoke!(statement_key, actor: actor, because: because, subject: subject,
|
|
327
|
+
tenant: tenant, acting_for: acting_for,
|
|
328
|
+
http_request: http_request)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def supersede!(statement_key, actor:, because: nil, subject: nil, tenant: nil,
|
|
332
|
+
acting_for: nil, http_request: nil)
|
|
333
|
+
Lifecycle.supersede!(statement_key, actor: actor, because: because, subject: subject,
|
|
334
|
+
tenant: tenant, acting_for: acting_for,
|
|
335
|
+
http_request: http_request)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# An explicitly recorded system exemption. Seeds, imports, invitations, and
|
|
339
|
+
# service accounts must never "accept" by omitting a browser parameter or by
|
|
340
|
+
# fabricating a human click. An exemption says plainly that no human action
|
|
341
|
+
# occurred, records who created it and why, and never satisfies
|
|
342
|
+
# `agreed_to?` — it answers the separate `exempted_from?` question.
|
|
343
|
+
def exempt!(policy_key, actor:, because:, subject: nil, tenant: nil)
|
|
344
|
+
Lifecycle.exempt!(policy_key, actor: actor, because: because, subject: subject,
|
|
345
|
+
tenant: tenant)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Captures evidence and commits a pending outbox row in one local
|
|
349
|
+
# transaction, for an action that has to cross a system boundary. This is a
|
|
350
|
+
# distributed reliability protocol, not a cross-system ACID transaction —
|
|
351
|
+
# see Clickwrap::Services::AuthorizeExternalAction for exactly what it does
|
|
352
|
+
# and does not promise.
|
|
353
|
+
def authorize_external_action!(policy_key,
|
|
354
|
+
after_pending_action_is_saved_inside_transaction: nil,
|
|
355
|
+
**,
|
|
356
|
+
&local_transaction_block)
|
|
357
|
+
if after_pending_action_is_saved_inside_transaction && local_transaction_block
|
|
358
|
+
raise ArgumentError,
|
|
359
|
+
"Pass either after_pending_action_is_saved_inside_transaction: or a block, not both."
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
local_transaction_hook =
|
|
363
|
+
after_pending_action_is_saved_inside_transaction || local_transaction_block
|
|
364
|
+
|
|
365
|
+
Services::AuthorizeExternalAction.new(
|
|
366
|
+
policy: policy!(policy_key),
|
|
367
|
+
after_pending_action_is_saved_inside_transaction: local_transaction_hook,
|
|
368
|
+
**
|
|
369
|
+
).call
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def import_external_receipt!(policy_key, actor:, provider_name:, provider_event_id:,
|
|
373
|
+
provider_receipt: nil, verified_with: nil, verified_at: nil,
|
|
374
|
+
occurred_at: nil, subject: nil, tenant: nil, because: nil,
|
|
375
|
+
statements: nil)
|
|
376
|
+
Import::ExternalReceipt.new(
|
|
377
|
+
policy: policy!(policy_key), actor: actor, provider_name: provider_name,
|
|
378
|
+
provider_event_id: provider_event_id, provider_receipt: provider_receipt,
|
|
379
|
+
verified_with: verified_with, verified_at: verified_at, occurred_at: occurred_at,
|
|
380
|
+
subject: subject, tenant: tenant, because: because, statements: statements
|
|
381
|
+
).import!
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def import_legacy!(policy_key, actor:, occurred_at:, because:, known: {}, unknown: [],
|
|
385
|
+
dry_run: false, subject: nil, tenant: nil, statements: nil,
|
|
386
|
+
capture_channel: "imported_provider", source: nil, counts_as_current: true)
|
|
387
|
+
Import::Legacy.new(
|
|
388
|
+
policy: policy!(policy_key), actor: actor, occurred_at: occurred_at, because: because,
|
|
389
|
+
known: known, unknown: unknown, dry_run: dry_run, subject: subject, tenant: tenant,
|
|
390
|
+
statements: statements, capture_channel: capture_channel, source: source,
|
|
391
|
+
counts_as_current: counts_as_current
|
|
392
|
+
).import!
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# --- Verification and gating ---------------------------------------------
|
|
396
|
+
|
|
397
|
+
def verify(policy_or_event, **) = Verification.verify(policy_or_event, **)
|
|
398
|
+
|
|
399
|
+
def require!(policy_key, **)
|
|
400
|
+
result = verify(policy_key, **)
|
|
401
|
+
raise VerificationFailed, result unless result.success?
|
|
402
|
+
|
|
403
|
+
result
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def current?(policy_key, **) = verify(policy_key, **).success?
|
|
407
|
+
|
|
408
|
+
# True when the actor needs to complete this policy: either they have no
|
|
409
|
+
# current evidence, or a newer required document version has published since
|
|
410
|
+
# they last acted. The application decides which change is material;
|
|
411
|
+
# Clickwrap enforces the rule it is given.
|
|
412
|
+
def required?(policy_key, **) = !current?(policy_key, **)
|
|
413
|
+
|
|
414
|
+
def receipt(event_id) = Receipt.find(event_id)
|
|
415
|
+
|
|
416
|
+
def export_receipt(receipt, **) = Receipt.export(receipt, **)
|
|
417
|
+
|
|
418
|
+
# Retry optional timestamp/anchor work that left no immutable result after a
|
|
419
|
+
# committed event. This is intentionally explicit: it can call external
|
|
420
|
+
# providers, so applications normally run it from a scheduled job or the
|
|
421
|
+
# matching rake task rather than hiding it in a read path.
|
|
422
|
+
def reconcile_missing_integrity_attestations!(scope: Event.all, retry_failed_attestations: false)
|
|
423
|
+
Integrity::AttestationReconciler.new(
|
|
424
|
+
scope: scope,
|
|
425
|
+
retry_failed_attestations: retry_failed_attestations
|
|
426
|
+
).call
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# --- Disposition ----------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
def delete_recorded_ip_address!(receipt, because:)
|
|
432
|
+
Retention::Disposition.delete_field!(receipt, :ip_address, because:)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def delete_recorded_browser_user_agent!(receipt, because:)
|
|
436
|
+
Retention::Disposition.delete_field!(receipt, :browser_user_agent, because:)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def delete_recorded_ip_geolocation!(receipt, because:)
|
|
440
|
+
Retention::Disposition.delete_field!(receipt, :ip_geolocation, because:)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# --- Actors ---------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
# A stable opaque identifier for someone who is not a persisted record. The
|
|
446
|
+
# host owns the identifier and any later account linking. An IP address is
|
|
447
|
+
# not an actor identifier and Clickwrap will not accept one here.
|
|
448
|
+
def anonymous_actor(identifier) = AnonymousActor.new(identifier)
|
|
449
|
+
|
|
450
|
+
# A named non-human actor, for seeds, imports, and background processes.
|
|
451
|
+
def system_actor(name) = SystemActor.new(name)
|
|
452
|
+
|
|
453
|
+
# --- Publishing -----------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
def publish!(dry_run: false) = Services::PublishDocuments.new(dry_run:).call
|
|
456
|
+
|
|
457
|
+
def doctor = Doctor.new.report
|
|
458
|
+
|
|
459
|
+
# --- Internals ------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
def gem_version = VERSION
|
|
462
|
+
def canonical_schema_version = CANONICAL_SCHEMA_VERSION
|
|
463
|
+
|
|
464
|
+
# The server's own clock, used for every recorded time. It is described in
|
|
465
|
+
# receipts as exactly that — time recorded by the application server — and
|
|
466
|
+
# never as trusted time, which is a different thing supplied by a different
|
|
467
|
+
# kind of provider.
|
|
468
|
+
def now = Time.now.utc
|
|
469
|
+
|
|
470
|
+
def logger
|
|
471
|
+
defined?(::Rails) && ::Rails.respond_to?(:logger) ? ::Rails.logger : nil
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Post-commit hooks are observers, never authorization. A failure here is
|
|
475
|
+
# reported and swallowed, because the evidence and the action it protected
|
|
476
|
+
# have already committed and nothing an analytics call does may undo them.
|
|
477
|
+
def report_after_commit_failure(error, event)
|
|
478
|
+
config.report_after_commit_failure_with.call(error, event)
|
|
479
|
+
rescue StandardError => error
|
|
480
|
+
logger&.error("[clickwrap] after-commit failure reporter itself raised: #{error.class}")
|
|
481
|
+
nil
|
|
482
|
+
end
|
|
483
|
+
end
|
|
15
484
|
end
|
|
485
|
+
|
|
486
|
+
require_relative "clickwrap/registry"
|
|
487
|
+
require_relative "clickwrap/localized_text"
|
|
488
|
+
require_relative "clickwrap/document_definition"
|
|
489
|
+
require_relative "clickwrap/statement"
|
|
490
|
+
require_relative "clickwrap/request_evidence_policy"
|
|
491
|
+
require_relative "clickwrap/policy"
|
|
492
|
+
require_relative "clickwrap/retention_class"
|
|
493
|
+
require_relative "clickwrap/dsl/policy_builder"
|
|
494
|
+
require_relative "clickwrap/dsl/retention_builder"
|
|
495
|
+
|
|
496
|
+
# The framework-integration modules are spine files, not autoloaded code: the
|
|
497
|
+
# engine's `on_load(:action_view)` / `on_load(:action_controller)` hooks run
|
|
498
|
+
# IMMEDIATELY when a host's other gems have already loaded that framework by
|
|
499
|
+
# the time Clickwrap's initializers register — which can be before the host's
|
|
500
|
+
# autoloader can serve these constants. They must exist the moment the engine
|
|
501
|
+
# file does.
|
|
502
|
+
require_relative "clickwrap/form_builder_extensions"
|
|
503
|
+
require_relative "clickwrap/view_helpers"
|
|
504
|
+
require_relative "clickwrap/controller_helpers"
|
|
505
|
+
require_relative "clickwrap/registration"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module Clickwrap
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate clickwrap:document terms` — declares one document and
|
|
8
|
+
# creates the file its bytes will come from, with the file's own front
|
|
9
|
+
# matter naming its version.
|
|
10
|
+
#
|
|
11
|
+
# Declaring is not publishing. This generator adds a declaration and a
|
|
12
|
+
# placeholder file; `bin/rails clickwrap:publish` is what reads the bytes,
|
|
13
|
+
# digests them, and freezes the snapshot that receipts point at from then
|
|
14
|
+
# on (deploys do this automatically after `db:prepare`).
|
|
15
|
+
#
|
|
16
|
+
# Changing a document later is NOT another run of this generator: the file
|
|
17
|
+
# that holds the words also names its version, so a new version is one
|
|
18
|
+
# edit in one file — change the text, bump `last_updated:` (or add
|
|
19
|
+
# `clickwrap_version:` for a same-day correction), publish. The
|
|
20
|
+
# declaration never changes, published bytes are never edited in place,
|
|
21
|
+
# and every receipt goes on pointing at the exact version its accepted
|
|
22
|
+
# server offer bound.
|
|
23
|
+
class DocumentGenerator < Rails::Generators::Base
|
|
24
|
+
source_root File.expand_path("templates", __dir__)
|
|
25
|
+
desc "Declare a clickwrap document and create its content file"
|
|
26
|
+
|
|
27
|
+
argument :name, type: :string, banner: "DOCUMENT_NAME"
|
|
28
|
+
|
|
29
|
+
class_option :document_version, type: :string,
|
|
30
|
+
desc: "An explicit version label, written into the file's " \
|
|
31
|
+
"front matter (defaults to today's date as last_updated)"
|
|
32
|
+
class_option :locale, type: :string,
|
|
33
|
+
desc: "Declare this version for one locale (en, es…)"
|
|
34
|
+
|
|
35
|
+
def create_content_file
|
|
36
|
+
if host_file?(content_path)
|
|
37
|
+
say_status :skip, "#{content_path} (you already have a file there)", :yellow
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
create_file content_path, placeholder_content
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def declare_document
|
|
45
|
+
create_policy_file_if_missing
|
|
46
|
+
|
|
47
|
+
if declaration_present?
|
|
48
|
+
say_status :skip, "#{policy_file} (:#{document_key} is already declared — a new " \
|
|
49
|
+
"version is a front-matter bump in #{content_path}, not a new " \
|
|
50
|
+
"declaration)", :yellow
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
append_to_file policy_file, declaration
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def display_next_steps
|
|
58
|
+
say "\n☑️ Document :#{document_key} declared.", :green
|
|
59
|
+
say "\nTo finish:"
|
|
60
|
+
say " 1. Put the real text in #{content_path}, keeping the front matter at the top."
|
|
61
|
+
say " Clickwrap never writes legal text — the words are yours."
|
|
62
|
+
say " 2. Run 'bin/rails clickwrap:publish' to freeze an immutable snapshot."
|
|
63
|
+
say " Deploys do this for you — publishing rides `db:prepare`."
|
|
64
|
+
say " 3. Reference it from a policy in #{policy_file}."
|
|
65
|
+
say "\nThe file names its own version: its `last_updated:` front matter is the label,"
|
|
66
|
+
say "so changing the text later is one edit in one file — new words, bumped label,"
|
|
67
|
+
say "publish. Reusing a label for different bytes is refused rather than accepted,"
|
|
68
|
+
say "and old receipts go on pointing at the bytes their accepted server offers bound.\n"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def document_key
|
|
74
|
+
name.to_s.underscore.tr("-", "_").tr("/", "_")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def explicit_version_label
|
|
78
|
+
options[:document_version].presence
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def locale
|
|
82
|
+
options[:locale].presence
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def policy_file
|
|
86
|
+
"config/clickwrap.rb"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def content_path
|
|
90
|
+
return "app/content/legal/#{document_key}.#{locale}.md" if locale
|
|
91
|
+
|
|
92
|
+
"app/content/legal/#{document_key}.md"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def declaration
|
|
96
|
+
lines = ["\nClickwrap.document :#{document_key},"]
|
|
97
|
+
lines << " locale: :#{locale}," if locale
|
|
98
|
+
lines << " from: Rails.root.join(#{content_path.inspect})\n"
|
|
99
|
+
lines.join("\n")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# One declaration per document key (and per locale): the version lives in
|
|
103
|
+
# the file, so a "new version" never adds a declaration — and a second
|
|
104
|
+
# version-less declaration of the same key would be refused at boot as a
|
|
105
|
+
# duplicate anyway.
|
|
106
|
+
def declaration_present?
|
|
107
|
+
File.read(File.expand_path(policy_file, destination_root))
|
|
108
|
+
.include?("Clickwrap.document :#{document_key},#{"\n locale: :#{locale}," if locale}")
|
|
109
|
+
rescue StandardError
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def create_policy_file_if_missing
|
|
114
|
+
return if host_file?(policy_file)
|
|
115
|
+
|
|
116
|
+
create_file policy_file, <<~RUBY
|
|
117
|
+
# frozen_string_literal: true
|
|
118
|
+
|
|
119
|
+
# Clickwrap documents, policies, and retention classes. Ordinary Ruby, so it is
|
|
120
|
+
# reviewable in a pull request and deploys with the code that depends on it.
|
|
121
|
+
RUBY
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def host_file?(path)
|
|
125
|
+
File.exist?(File.expand_path(path, destination_root))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The front matter is the version label. `last_updated:` is the usual
|
|
129
|
+
# key; an explicit --document-version lands as `clickwrap_version:`,
|
|
130
|
+
# which outranks it, so a hand-chosen label and a human-facing date can
|
|
131
|
+
# coexist without fighting.
|
|
132
|
+
def placeholder_front_matter
|
|
133
|
+
lines = ["---", "title: #{document_key.humanize}"]
|
|
134
|
+
lines << "clickwrap_version: #{explicit_version_label}" if explicit_version_label
|
|
135
|
+
lines << "last_updated: #{Date.today.iso8601}"
|
|
136
|
+
lines << "---"
|
|
137
|
+
"#{lines.join("\n")}\n\n"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def placeholder_content
|
|
141
|
+
<<~MARKDOWN
|
|
142
|
+
#{placeholder_front_matter.rstrip}
|
|
143
|
+
|
|
144
|
+
# PLACEHOLDER — replace this with your own reviewed text
|
|
145
|
+
|
|
146
|
+
**This is not a #{document_key.humanize.downcase}.** The `clickwrap` gem created this
|
|
147
|
+
file so the declaration in `config/clickwrap.rb` has bytes to point at, and it
|
|
148
|
+
deliberately contains no legal text of any kind: the gem does not know who you
|
|
149
|
+
are, where you operate, or which rules apply, and text that merely looked
|
|
150
|
+
plausible would be worse than this, because someone would ship it.
|
|
151
|
+
|
|
152
|
+
Replace this file, then run `bin/rails clickwrap:publish` to freeze an immutable
|
|
153
|
+
snapshot. Publishing records the exact bytes, media type, locale, and digest, so
|
|
154
|
+
a receipt written today can reproduce the document version its accepted server
|
|
155
|
+
offer bound years from now.
|
|
156
|
+
|
|
157
|
+
The front matter above is the version label: when the text changes, bump
|
|
158
|
+
`last_updated:` (or add `clickwrap_version:` for a same-day correction) and
|
|
159
|
+
publish again, rather than editing a published version in place.
|
|
160
|
+
MARKDOWN
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|