Module: Clickwrap

Defined in:
lib/clickwrap.rb,
lib/clickwrap/digest.rb,
lib/clickwrap/doctor.rb,
lib/clickwrap/engine.rb,
lib/clickwrap/errors.rb,
lib/clickwrap/linter.rb,
lib/clickwrap/macros.rb,
lib/clickwrap/policy.rb,
lib/clickwrap/capture.rb,
lib/clickwrap/privacy.rb,
lib/clickwrap/receipt.rb,
lib/clickwrap/testing.rb,
lib/clickwrap/version.rb,
lib/clickwrap/registry.rb,
lib/clickwrap/authority.rb,
lib/clickwrap/lifecycle.rb,
lib/clickwrap/presenter.rb,
lib/clickwrap/reference.rb,
lib/clickwrap/statement.rb,
lib/clickwrap/identifier.rb,
lib/clickwrap/submission.rb,
lib/clickwrap/vocabulary.rb,
lib/clickwrap/actor_proxy.rb,
lib/clickwrap/front_matter.rb,
lib/clickwrap/models/event.rb,
lib/clickwrap/receipt_html.rb,
lib/clickwrap/registration.rb,
lib/clickwrap/system_actor.rb,
lib/clickwrap/test_helpers.rb,
lib/clickwrap/verification.rb,
lib/clickwrap/view_helpers.rb,
lib/clickwrap/configuration.rb,
lib/clickwrap/current_state.rb,
lib/clickwrap/import/legacy.rb,
lib/clickwrap/reviewed_text.rb,
lib/clickwrap/canonical_json.rb,
lib/clickwrap/localized_text.rb,
lib/clickwrap/anonymous_actor.rb,
lib/clickwrap/integrity/chain.rb,
lib/clickwrap/models/document.rb,
lib/clickwrap/pending_receipt.rb,
lib/clickwrap/retention_class.rb,
lib/clickwrap/integrity/anchor.rb,
lib/clickwrap/receipt_verifier.rb,
lib/clickwrap/document_renderer.rb,
lib/clickwrap/import/fine_print.rb,
lib/clickwrap/models/chain_head.rb,
lib/clickwrap/models/legal_hold.rb,
lib/clickwrap/protected_outcome.rb,
lib/clickwrap/remediation_token.rb,
lib/clickwrap/retention/applier.rb,
lib/clickwrap/retention/planner.rb,
lib/clickwrap/controller_helpers.rb,
lib/clickwrap/dsl/policy_builder.rb,
lib/clickwrap/integrity/attestor.rb,
lib/clickwrap/document_definition.rb,
lib/clickwrap/integrity/timestamp.rb,
lib/clickwrap/models/presentation.rb,
lib/clickwrap/schema_requirements.rb,
lib/clickwrap/subject_fingerprint.rb,
app/helpers/clickwrap/engine_helper.rb,
lib/clickwrap/capture/event_builder.rb,
lib/clickwrap/dsl/retention_builder.rb,
lib/clickwrap/models/event_document.rb,
lib/clickwrap/models/receipt_access.rb,
lib/clickwrap/presentation_manifest.rb,
lib/clickwrap/retention/disposition.rb,
lib/clickwrap/models/event_statement.rb,
lib/clickwrap/models/external_action.rb,
lib/clickwrap/models/policy_revision.rb,
lib/clickwrap/models/statement_state.rb,
lib/clickwrap/services/load_policies.rb,
lib/clickwrap/durable_commit_callback.rb,
lib/clickwrap/form_builder_extensions.rb,
lib/clickwrap/import/external_receipt.rb,
lib/clickwrap/ip_geolocation/location.rb,
lib/clickwrap/ip_geolocation/resolver.rb,
lib/clickwrap/models/disposition_plan.rb,
lib/clickwrap/models/document_version.rb,
lib/clickwrap/models/request_evidence.rb,
lib/clickwrap/request_evidence_policy.rb,
lib/clickwrap/models/application_record.rb,
lib/clickwrap/models/recording_sequence.rb,
lib/generators/clickwrap/link_generator.rb,
lib/clickwrap/request_evidence_extractor.rb,
lib/clickwrap/services/publish_documents.rb,
lib/generators/clickwrap/views_generator.rb,
lib/clickwrap/document_renderers/markdown.rb,
lib/clickwrap/trusted_proxy_configuration.rb,
lib/generators/clickwrap/policy_generator.rb,
lib/clickwrap/ip_geolocation/null_resolver.rb,
lib/clickwrap/models/integrity_attestation.rb,
lib/generators/clickwrap/install_generator.rb,
lib/generators/clickwrap/upgrade_generator.rb,
lib/clickwrap/capture/presentation_verifier.rb,
lib/generators/clickwrap/document_generator.rb,
lib/clickwrap/ip_geolocation/static_resolver.rb,
lib/clickwrap/models/concerns/has_clickwraps.rb,
lib/clickwrap/models/statement_identity_lock.rb,
lib/generators/clickwrap/hardening_generator.rb,
app/controllers/clickwrap/captures_controller.rb,
app/controllers/clickwrap/receipts_controller.rb,
lib/clickwrap/integrity/attestation_reconciler.rb,
lib/clickwrap/document_renderers/markdown_rails.rb,
lib/clickwrap/ip_geolocation/trackdown_resolver.rb,
app/controllers/clickwrap/application_controller.rb,
app/controllers/clickwrap/withdrawals_controller.rb,
lib/clickwrap/services/authorize_external_action.rb,
lib/clickwrap/services/validate_policy_references.rb,
lib/clickwrap/integrations/organizations_authority.rb,
app/controllers/clickwrap/document_versions_controller.rb

Overview

Clickwrap

The evidence-and-assent layer for Rails: versioned agreements, acknowledgments, consent, declarations, attestations, and authorizations, captured with the exact content and presentation they were offered under, and committed in the same transaction as the action they authorize.

The public surface is intentionally small:

Clickwrap.configure { |config| ... }   # one block, in an initializer
Clickwrap.document :terms, ...         # immutable versioned content
Clickwrap.policy :signup do ... end    # what the server offers and accepts
Clickwrap.retention :ordinary do ...   # how long each part is kept

has_clickwraps                         # on the model that can act
form.clickwrap :signup, submit: "..."  # render the controls and the action

Clickwrap.capture!(:signup, actor:, http_request:, submission:)
Clickwrap.capture_and!(:withdrawal, ...) { withdrawal.submit! }
user.clickwraps.agreed_to?(:terms)
Clickwrap.verify(:withdrawal, actor:, subject:)

What this gem does is evidence mechanics. What it does not do is decide whether your agreement is enforceable, whether consent is the right lawful basis, whether a document change is material, who someone really is, or how long you must keep anything. Those belong to the application and its counsel, and no configuration flag here can stand in for them.

Defined Under Namespace

Modules: Authority, CanonicalJson, ControllerHelpers, CurrentState, DSL, Digest, DocumentRenderers, EngineHelper, FormBuilderExtensions, FrontMatter, Generators, HasClickwraps, Identifier, Import, Integrations, Integrity, IpGeolocation, Lifecycle, Macros, Privacy, ProtectedOutcome, ReceiptVerifier, Reference, Registration, Retention, ReviewedText, SchemaRequirements, Services, SubjectFingerprint, TestHelpers, Testing, TrustedProxyConfiguration, Verification, ViewHelpers, Vocabulary Classes: AccessNotAuthorized, ActorProxy, AlreadyConsumedError, AlreadyWithdrawnError, AnonymousActor, AnswerInvalid, ApplicationController, ApplicationRecord, AuthorityDecision, AuthorityNotVerified, AuthorityRule, AuthorityVerifier, Capture, CaptureRefused, CapturesController, ChainHead, Configuration, ConfigurationError, DefinitionError, DispositionPlan, DispositionPlanInvalid, Doctor, Document, DocumentDefinition, DocumentDigestMismatchError, DocumentNotPublishedError, DocumentRenderer, DocumentVersion, DocumentVersionConflictError, DocumentVersionsController, DurableCommitCallback, Engine, Error, Event, EventDocument, EventStatement, EventWriteFailed, ExternalAction, ExternalActionAlreadyResolved, ExternalActionError, ImmutableEvidenceError, IntegrityAttestation, IntegrityCheckFailed, LegalHold, LegalHoldInEffect, LifecycleError, Linter, LocalizedText, MissingTranslation, NotDefinedError, NotWithdrawableError, OneTimeAuthorizationConflict, PendingReceipt, Policy, PolicyRevision, Presentation, PresentationExpired, PresentationInvalid, PresentationManifest, Presenter, Receipt, ReceiptAccess, ReceiptHtml, ReceiptInvalid, ReceiptNotCommitted, ReceiptsController, RecordingSequence, RegistrationFailed, Registry, RemediationInvalid, RemediationNotAuthorized, RemediationToken, ReplayRejected, RepresentedPartyCreationFailed, RequestEvidence, RequestEvidenceExtractor, RequestEvidencePolicy, RequestEvidenceUnavailable, RetentionClass, RetryableTransactionError, Statement, StatementIdentityLock, StatementState, Submission, SubmissionInvalid, SystemActor, UnknownPolicyError, UnknownReceiptSchema, UnknownRetentionClassError, UnknownStatementError, VerificationFailed, WithdrawalsController

Constant Summary collapse

DOCUMENT_OPTIONS =
%i[
  version locale media_type effective_at tenant from content resolver renderer link
].freeze
VERSION =
"0.1.1"
CANONICAL_SCHEMA_VERSION =

The canonical schema version for receipts, event digests, and presentation manifests. This is deliberately independent of VERSION: gem releases may come and go without changing how historical evidence is serialized, and a change here always means a new explicit schema plus a verifier that still reads every previously released version.

"clickwrap.receipt.v1"
VERIFIER_VERSION =

Bumped only when the receiver-side verification logic changes in a way an auditor should be able to see in a receipt.

"1"
AUTHORITY_DECISION_ATTRIBUTES =
%i[authorized source role verified_at details].freeze

Class Method Summary collapse

Class Method Details

.anonymous_actor(identifier) ⇒ Object

A stable opaque identifier for someone who is not a persisted record. The host owns the identifier and any later account linking. An IP address is not an actor identifier and Clickwrap will not accept one here.



448
# File 'lib/clickwrap.rb', line 448

def anonymous_actor(identifier) = AnonymousActor.new(identifier)

.authorize_external_action!(policy_key, after_pending_action_is_saved_inside_transaction: nil, &local_transaction_block) ⇒ Object

Captures evidence and commits a pending outbox row in one local transaction, for an action that has to cross a system boundary. This is a distributed reliability protocol, not a cross-system ACID transaction — see Clickwrap::Services::AuthorizeExternalAction for exactly what it does and does not promise.



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/clickwrap.rb', line 353

def authorize_external_action!(policy_key,
                               after_pending_action_is_saved_inside_transaction: nil,
                               **,
                               &local_transaction_block)
  if after_pending_action_is_saved_inside_transaction && local_transaction_block
    raise ArgumentError,
          "Pass either after_pending_action_is_saved_inside_transaction: or a block, not both."
  end

  local_transaction_hook =
    after_pending_action_is_saved_inside_transaction || local_transaction_block

  Services::AuthorizeExternalAction.new(
    policy: policy!(policy_key),
    after_pending_action_is_saved_inside_transaction: local_transaction_hook,
    **
  ).call
end

.canonical_schema_versionObject



462
# File 'lib/clickwrap.rb', line 462

def canonical_schema_version = CANONICAL_SCHEMA_VERSION

.capture!(policy_key, actor:, subject: nil, tenant: nil, http_request: nil, submission: nil, answers: nil, locale: nil, capture_channel: nil, acting_for: nil, authentication_context: nil, attribution_method: nil, idempotency_key: nil) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/clickwrap.rb', line 191

def capture!(policy_key, actor:, subject: nil, tenant: nil, http_request: nil,
             submission: nil, answers: nil, locale: nil, capture_channel: nil,
             acting_for: nil, authentication_context: nil, attribution_method: nil,
             idempotency_key: nil)
  Capture.new(
    policy: policy!(policy_key), actor: actor, subject: subject, tenant: tenant,
    http_request: http_request, submission: submission, answers: answers, locale: locale,
    capture_channel: capture_channel, acting_for: acting_for,
    authentication_context: authentication_context, attribution_method: attribution_method,
    idempotency_key: idempotency_key
  ).capture!
end

.capture_and!(policy_key, actor:, subject: nil, tenant: nil, http_request: nil, submission: nil, answers: nil, locale: nil, capture_channel: nil, acting_for: nil, authentication_context: nil, attribution_method: nil, idempotency_key: nil) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/clickwrap.rb', line 204

def capture_and!(policy_key, actor:, subject: nil, tenant: nil, http_request: nil,
                 submission: nil, answers: nil, locale: nil, capture_channel: nil,
                 acting_for: nil, authentication_context: nil, attribution_method: nil,
                 idempotency_key: nil, &)
  Capture.new(
    policy: policy!(policy_key), actor: actor, subject: subject, tenant: tenant,
    http_request: http_request, submission: submission, answers: answers, locale: locale,
    capture_channel: capture_channel, acting_for: acting_for,
    authentication_context: authentication_context, attribution_method: attribution_method,
    idempotency_key: idempotency_key
  ).capture_and!(&)
end


315
316
317
318
319
320
321
322
# File 'lib/clickwrap.rb', line 315

def change_consent_scope!(statement_key, actor:, because:, subject: nil, tenant: nil,
                          acting_for: nil, http_request: nil, submission: nil, answers: nil)
  Lifecycle.change_consent_scope!(statement_key, actor: actor, because: because,
                                                 subject: subject, tenant: tenant,
                                                 acting_for: acting_for,
                                                 http_request: http_request,
                                                 submission: submission, answers: answers)
end

.configObject Also known as: configuration

--- Configuration --------------------------------------------------------



78
79
80
# File 'lib/clickwrap.rb', line 78

def config
  @config ||= Configuration.new
end

.configure {|config| ... } ⇒ Object

Yields:



84
85
86
87
88
# File 'lib/clickwrap.rb', line 84

def configure
  yield config if block_given?
  config.validate!
  config
end

.correct_declaration!(statement_key, actor:, because:, subject: nil, tenant: nil, replaces: nil, acting_for: nil, http_request: nil, submission: nil, answers: nil) ⇒ Object

Correcting, renewing, and rescoping are new statements by the same person, so each takes the same submission: a first capture does: they are captured through a real presentation, not flipped administratively.



298
299
300
301
302
303
304
305
# File 'lib/clickwrap.rb', line 298

def correct_declaration!(statement_key, actor:, because:, subject: nil, tenant: nil,
                         replaces: nil, acting_for: nil, http_request: nil,
                         submission: nil, answers: nil)
  Lifecycle.correct!(statement_key, actor: actor, because: because, subject: subject,
                                    tenant: tenant, replaces: replaces, acting_for: acting_for,
                                    http_request: http_request, submission: submission,
                                    answers: answers)
end

.create_represented_party!(policy_key, actor:, represented_party:, represented_party_creation_flow_id:, subject: nil, tenant: nil, http_request: nil, submission: nil, answers: nil, locale: nil, capture_channel: nil, authentication_context: nil, attribution_method: nil, idempotency_key: nil) ⇒ Object

A prospective represented-party flow for records such as a new customer organization. The form binds the record type and a server-owned browser flow before the record exists. This block must return a persisted record of that presented type after creating its host authority relationship; Clickwrap then verifies authority, rebinds the final stable reference, and commits all of it together.



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/clickwrap.rb', line 223

def create_represented_party!(policy_key, actor:, represented_party:,
                              represented_party_creation_flow_id:,
                              subject: nil, tenant: nil, http_request: nil,
                              submission: nil, answers: nil, locale: nil,
                              capture_channel: nil, authentication_context: nil,
                              attribution_method: nil, idempotency_key: nil, &)
  Capture.new(
    policy: policy!(policy_key), actor: actor, subject: subject, tenant: tenant,
    http_request: http_request, submission: submission, answers: answers, locale: locale,
    capture_channel: capture_channel, acting_for: represented_party,
    authentication_context: authentication_context, attribution_method: attribution_method,
    idempotency_key: idempotency_key,
    represented_party_creation_flow_id: represented_party_creation_flow_id
  ).create_represented_party!(&)
end

.current?(policy_key) ⇒ Boolean

Returns:

  • (Boolean)


406
# File 'lib/clickwrap.rb', line 406

def current?(policy_key, **) = verify(policy_key, **).success?

.delete_recorded_browser_user_agent!(receipt, because:) ⇒ Object



435
436
437
# File 'lib/clickwrap.rb', line 435

def delete_recorded_browser_user_agent!(receipt, because:)
  Retention::Disposition.delete_field!(receipt, :browser_user_agent, because:)
end

.delete_recorded_ip_address!(receipt, because:) ⇒ Object

--- Disposition ----------------------------------------------------------



431
432
433
# File 'lib/clickwrap.rb', line 431

def delete_recorded_ip_address!(receipt, because:)
  Retention::Disposition.delete_field!(receipt, :ip_address, because:)
end

.delete_recorded_ip_geolocation!(receipt, because:) ⇒ Object



439
440
441
# File 'lib/clickwrap.rb', line 439

def delete_recorded_ip_geolocation!(receipt, because:)
  Retention::Disposition.delete_field!(receipt, :ip_geolocation, because:)
end

.doctorObject



457
# File 'lib/clickwrap.rb', line 457

def doctor = Doctor.new.report

.document(key, **options) ⇒ Object

Declares one immutable document version. Declaring it does not publish it: bin/rails clickwrap:publish reads the bytes once, digests them, and freezes a database snapshot. Until then the declaration is a promise about what will be published, and a policy that references an unpublished document fails loudly rather than presenting nothing.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/clickwrap.rb', line 119

def document(key, **options)
  unknown = options.keys.map(&:to_sym) - DOCUMENT_OPTIONS
  unless unknown.empty?
    raise DefinitionError,
          "Document #{key.inspect} has unknown option#{"s" if unknown.many?} " \
          "#{unknown.map { |option| "`#{option}:`" }.join(", ")}. Supported options are: " \
          "#{DOCUMENT_OPTIONS.map { |option| "`#{option}:`" }.join(", ")}. Clickwrap never " \
          "ignores document options."
  end

  definition = DocumentDefinition.new(key: key, **options)
  documents.register(definition.identity, definition)
  definition
end

.document_definitions_for(key, tenant: nil) ⇒ Object



179
180
181
182
183
# File 'lib/clickwrap.rb', line 179

def document_definitions_for(key, tenant: nil)
  documents.values.select do |definition|
    definition.key == key.to_s && definition.tenant_key == tenant&.to_s
  end
end

.documentsObject

--- Registries -----------------------------------------------------------



110
# File 'lib/clickwrap.rb', line 110

def documents = @documents ||= Registry.new(:document)

.exempt!(policy_key, actor:, because:, subject: nil, tenant: nil) ⇒ Object

An explicitly recorded system exemption. Seeds, imports, invitations, and service accounts must never "accept" by omitting a browser parameter or by fabricating a human click. An exemption says plainly that no human action occurred, records who created it and why, and never satisfies agreed_to? — it answers the separate exempted_from? question.



343
344
345
346
# File 'lib/clickwrap.rb', line 343

def exempt!(policy_key, actor:, because:, subject: nil, tenant: nil)
  Lifecycle.exempt!(policy_key, actor: actor, because: because, subject: subject,
                                tenant: tenant)
end

.export_receipt(receipt) ⇒ Object



416
# File 'lib/clickwrap.rb', line 416

def export_receipt(receipt, **) = Receipt.export(receipt, **)

.gem_versionObject

--- Internals ------------------------------------------------------------



461
# File 'lib/clickwrap.rb', line 461

def gem_version = VERSION

.import_external_receipt!(policy_key, actor:, provider_name:, provider_event_id:, provider_receipt: nil, verified_with: nil, verified_at: nil, occurred_at: nil, subject: nil, tenant: nil, because: nil, statements: nil) ⇒ Object



372
373
374
375
376
377
378
379
380
381
382
# File 'lib/clickwrap.rb', line 372

def import_external_receipt!(policy_key, actor:, provider_name:, provider_event_id:,
                             provider_receipt: nil, verified_with: nil, verified_at: nil,
                             occurred_at: nil, subject: nil, tenant: nil, because: nil,
                             statements: nil)
  Import::ExternalReceipt.new(
    policy: policy!(policy_key), actor: actor, provider_name: provider_name,
    provider_event_id: provider_event_id, provider_receipt: provider_receipt,
    verified_with: verified_with, verified_at: verified_at, occurred_at: occurred_at,
    subject: subject, tenant: tenant, because: because, statements: statements
  ).import!
end

.import_legacy!(policy_key, actor:, occurred_at:, because:, known: {}, unknown: [], dry_run: false, subject: nil, tenant: nil, statements: nil, capture_channel: "imported_provider", source: nil, counts_as_current: true) ⇒ Object



384
385
386
387
388
389
390
391
392
393
# File 'lib/clickwrap.rb', line 384

def import_legacy!(policy_key, actor:, occurred_at:, because:, known: {}, unknown: [],
                   dry_run: false, subject: nil, tenant: nil, statements: nil,
                   capture_channel: "imported_provider", source: nil, counts_as_current: true)
  Import::Legacy.new(
    policy: policy!(policy_key), actor: actor, occurred_at: occurred_at, because: because,
    known: known, unknown: unknown, dry_run: dry_run, subject: subject, tenant: tenant,
    statements: statements, capture_channel: capture_channel, source: source,
    counts_as_current: counts_as_current
  ).import!
end

.loggerObject



470
471
472
# File 'lib/clickwrap.rb', line 470

def logger
  defined?(::Rails) && ::Rails.respond_to?(:logger) ? ::Rails.logger : nil
end

.nowObject

The server's own clock, used for every recorded time. It is described in receipts as exactly that — time recorded by the application server — and never as trusted time, which is a different thing supplied by a different kind of provider.



468
# File 'lib/clickwrap.rb', line 468

def now = Time.now.utc

.policiesObject



111
# File 'lib/clickwrap.rb', line 111

def policies = @policies ||= Registry.new(:policy)

.policy(key, &block) ⇒ Object

Declares a server-owned policy. Compiles immediately so a mistake is a boot failure with a sentence explaining it, not a surprise at 3am.

Raises:



136
137
138
139
140
141
142
143
144
145
# File 'lib/clickwrap.rb', line 136

def policy(key, &block)
  raise DefinitionError, "Clickwrap.policy needs a block" unless block

  builder = DSL::PolicyBuilder.new(key)
  builder.instance_eval(&block)
  compiled = builder.compile

  policies.register(compiled.key, compiled)
  compiled
end

.policy!(key) ⇒ Object



162
163
164
165
166
167
168
169
# File 'lib/clickwrap.rb', line 162

def policy!(key)
  policies.fetch(key.to_s) do
    raise UnknownPolicyError,
          "No policy named #{key.inspect}. Defined policies: " \
          "#{policies.keys.sort.join(", ").presence || "(none)"}. Policies are declared with " \
          "`Clickwrap.policy #{key.inspect} do ... end`, conventionally in config/clickwrap.rb."
  end
end

.present(policy_key) ⇒ Object

--- Presentation and capture --------------------------------------------



187
188
189
# File 'lib/clickwrap.rb', line 187

def present(policy_key, **)
  Presenter.new(policy: policy!(policy_key), **).present
end

.protected_outcomeObject

Produces the strict, canonical result snapshot consumed by record_protected_outcome_with. Keeping this construction in the gem prevents every host from inventing a subtly different hash contract.



265
# File 'lib/clickwrap.rb', line 265

def protected_outcome(...) = ProtectedOutcome.build(...)

.publish!(dry_run: false) ⇒ Object

--- Publishing -----------------------------------------------------------



455
# File 'lib/clickwrap.rb', line 455

def publish!(dry_run: false) = Services::PublishDocuments.new(dry_run:).call

.receipt(event_id) ⇒ Object



414
# File 'lib/clickwrap.rb', line 414

def receipt(event_id) = Receipt.find(event_id)

.reconcile_missing_integrity_attestations!(scope: Event.all, retry_failed_attestations: false) ⇒ Object

Retry optional timestamp/anchor work that left no immutable result after a committed event. This is intentionally explicit: it can call external providers, so applications normally run it from a scheduled job or the matching rake task rather than hiding it in a read path.



422
423
424
425
426
427
# File 'lib/clickwrap.rb', line 422

def reconcile_missing_integrity_attestations!(scope: Event.all, retry_failed_attestations: false)
  Integrity::AttestationReconciler.new(
    scope: scope,
    retry_failed_attestations: retry_failed_attestations
  ).call
end

.register!(policy_key, prospective_actor:, subject: nil, tenant: nil, http_request: nil, submission: nil, answers: nil, locale: nil, capture_channel: nil, acting_for: nil, authentication_context: nil, idempotency_key: nil, registration_flow_id: nil) ⇒ Object

Signup, modeled honestly: at first render there is no persisted actor, so the presentation binds to a short-lived registration flow, and the account and its evidence commit together. The receipt records account-registration attribution rather than pretending someone was already authenticated.

A prospective actor must be new. A public form's typed email address is not proof that its visitor controls an existing account or lead row. Use a distinct pending-request record, confirm the email address, and only then capture for the verified actor through the ordinary authenticated path.



249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/clickwrap.rb', line 249

def register!(policy_key, prospective_actor:, subject: nil, tenant: nil, http_request: nil,
              submission: nil, answers: nil, locale: nil, capture_channel: nil,
              acting_for: nil, authentication_context: nil, idempotency_key: nil,
              registration_flow_id: nil, &)
  Capture.new(
    policy: policy!(policy_key), actor: nil, prospective_actor: prospective_actor,
    subject: subject, tenant: tenant, http_request: http_request, submission: submission,
    answers: answers, locale: locale, capture_channel: capture_channel,
    acting_for: acting_for, authentication_context: authentication_context,
    idempotency_key: idempotency_key, registration_flow_id: registration_flow_id
  ).register!(&)
end

.renew!(statement_key, actor:, because:, subject: nil, tenant: nil, acting_for: nil, http_request: nil, submission: nil, answers: nil) ⇒ Object



307
308
309
310
311
312
313
# File 'lib/clickwrap.rb', line 307

def renew!(statement_key, actor:, because:, subject: nil, tenant: nil,
           acting_for: nil, http_request: nil, submission: nil, answers: nil)
  Lifecycle.renew!(statement_key, actor: actor, because: because, subject: subject,
                                  tenant: tenant, acting_for: acting_for,
                                  http_request: http_request, submission: submission,
                                  answers: answers)
end

.report_after_commit_failure(error, event) ⇒ Object

Post-commit hooks are observers, never authorization. A failure here is reported and swallowed, because the evidence and the action it protected have already committed and nothing an analytics call does may undo them.



477
478
479
480
481
482
# File 'lib/clickwrap.rb', line 477

def report_after_commit_failure(error, event)
  config.report_after_commit_failure_with.call(error, event)
rescue StandardError => error
  logger&.error("[clickwrap] after-commit failure reporter itself raised: #{error.class}")
  nil
end

.require!(policy_key) ⇒ Object

Raises:



399
400
401
402
403
404
# File 'lib/clickwrap.rb', line 399

def require!(policy_key, **)
  result = verify(policy_key, **)
  raise VerificationFailed, result unless result.success?

  result
end

.required?(policy_key) ⇒ Boolean

True when the actor needs to complete this policy: either they have no current evidence, or a newer required document version has published since they last acted. The application decides which change is material; Clickwrap enforces the rule it is given.

Returns:

  • (Boolean)


412
# File 'lib/clickwrap.rb', line 412

def required?(policy_key, **) = !current?(policy_key, **)

.reset!Object

Reset all global state. Used by the test suite to keep examples isolated; also handy in a console when experimenting with configuration.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/clickwrap.rb', line 92

def reset!
  @config = Configuration.new
  @documents = nil
  @policies = nil
  @retention_classes = nil
  # Every memoized verifier goes with the configuration it was built from.
  # A verifier that outlived a reset keeps signing and accepting tokens
  # under the previous secret, which is the kind of thing a test suite
  # papers over (by resetting it itself) and a console session discovers
  # the hard way.
  RemediationToken.reset_verifier! if defined?(RemediationToken)
  PresentationManifest.reset_verifier! if defined?(PresentationManifest)
  SchemaRequirements.reset! if defined?(SchemaRequirements)
  self
end

.retention(key, &block) ⇒ Object

Declares a retention class. Clickwrap does not choose retention periods and cannot tell you whether yours are right; it makes a reviewed decision executable, auditable, and separable — the core event's schedule is independent of the optional personal request evidence attached to it.

Raises:



151
152
153
154
155
156
157
158
159
160
# File 'lib/clickwrap.rb', line 151

def retention(key, &block)
  raise DefinitionError, "Clickwrap.retention needs a block" unless block

  builder = DSL::RetentionBuilder.new(key)
  builder.instance_eval(&block)
  compiled = builder.compile

  retention_classes.register(compiled.key, compiled)
  compiled
end

.retention_class!(key) ⇒ Object



171
172
173
174
175
176
177
# File 'lib/clickwrap.rb', line 171

def retention_class!(key)
  retention_classes.fetch(key.to_s) do
    raise UnknownRetentionClassError,
          "No retention class named #{key.inspect}. Defined classes: " \
          "#{retention_classes.keys.sort.join(", ").presence || "(none)"}."
  end
end

.retention_classesObject



112
# File 'lib/clickwrap.rb', line 112

def retention_classes = @retention_classes ||= Registry.new(:retention_class)

.revoke!(statement_key, actor:, because:, subject: nil, tenant: nil, acting_for: nil, http_request: nil) ⇒ Object



324
325
326
327
328
329
# File 'lib/clickwrap.rb', line 324

def revoke!(statement_key, actor:, because:, subject: nil, tenant: nil,
            acting_for: nil, http_request: nil)
  Lifecycle.revoke!(statement_key, actor: actor, because: because, subject: subject,
                                   tenant: tenant, acting_for: acting_for,
                                   http_request: http_request)
end

.submission_from(params) ⇒ Object



278
# File 'lib/clickwrap.rb', line 278

def submission_from(params, ...) = Submission.from_params(params, ...)

.supersede!(statement_key, actor:, because: nil, subject: nil, tenant: nil, acting_for: nil, http_request: nil) ⇒ Object



331
332
333
334
335
336
# File 'lib/clickwrap.rb', line 331

def supersede!(statement_key, actor:, because: nil, subject: nil, tenant: nil,
               acting_for: nil, http_request: nil)
  Lifecycle.supersede!(statement_key, actor: actor, because: because, subject: subject,
                                      tenant: tenant, acting_for: acting_for,
                                      http_request: http_request)
end

.system_actor(name) ⇒ Object

A named non-human actor, for seeds, imports, and background processes.



451
# File 'lib/clickwrap.rb', line 451

def system_actor(name) = SystemActor.new(name)

.trusted_proxy_configuration_digest_for(trusted_proxies) ⇒ Object

Digest the effective proxy rules rather than a prose description of them. The Rails-specific helper includes Rails' actual defaults when the application has not overridden action_dispatch.trusted_proxies.



270
271
272
# File 'lib/clickwrap.rb', line 270

def trusted_proxy_configuration_digest_for(trusted_proxies)
  TrustedProxyConfiguration.digest_for(trusted_proxies)
end

.trusted_proxy_configuration_digest_for_rails_application(application = Rails.application) ⇒ Object



274
275
276
# File 'lib/clickwrap.rb', line 274

def trusted_proxy_configuration_digest_for_rails_application(application = Rails.application)
  TrustedProxyConfiguration.digest_for_rails_application(application)
end

.verify(policy_or_event) ⇒ Object

--- Verification and gating ---------------------------------------------



397
# File 'lib/clickwrap.rb', line 397

def verify(policy_or_event, **) = Verification.verify(policy_or_event, **)

.withdraw!(purpose_key, actor:, because:, tenant: nil, subject: nil, acting_for: nil, http_request: nil) ⇒ Object

--- Lifecycle ------------------------------------------------------------

Each of these spells out the keywords its target accepts rather than forwarding **. A bare forward compiles fine and reads fine, and then an editor shows ** where the argument list should be, a typo'd keyword travels one method further before failing, and the public API of the gem is documented only in the private method behind it.



288
289
290
291
292
293
# File 'lib/clickwrap.rb', line 288

def withdraw!(purpose_key, actor:, because:, tenant: nil, subject: nil,
              acting_for: nil, http_request: nil)
  Lifecycle.withdraw!(purpose_key, actor: actor, because: because, tenant: tenant,
                                   subject: subject, acting_for: acting_for,
                                   http_request: http_request)
end