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

DEFAULT_RETENTION_CLASS_KEY =

The retention class every policy gets unless it names its own with retain_with: evidence kept indefinitely, deletion always an explicit, reviewed act. Keeping is reversible; deleting is not.

"evidence_kept_indefinitely"
DOCUMENT_OPTIONS =
%i[
  version locale media_type effective_at tenant from content resolver renderer link
].freeze
VERSION =
"0.3.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.



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

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.



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/clickwrap.rb', line 369

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



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

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



207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/clickwrap.rb', line 207

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



220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/clickwrap.rb', line 220

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


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

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



82
83
84
# File 'lib/clickwrap.rb', line 82

def config
  @config ||= Configuration.new
end

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

Yields:



88
89
90
91
92
# File 'lib/clickwrap.rb', line 88

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.



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

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.



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/clickwrap.rb', line 239

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)


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

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

.delete_recorded_browser_user_agent!(receipt, because:) ⇒ Object



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

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



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

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

.delete_recorded_ip_geolocation!(receipt, because:) ⇒ Object



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

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

.doctorObject



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

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.



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

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



195
196
197
198
199
# File 'lib/clickwrap.rb', line 195

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



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

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.



359
360
361
362
# File 'lib/clickwrap.rb', line 359

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



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

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

.gem_versionObject

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



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

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



388
389
390
391
392
393
394
395
396
397
398
# File 'lib/clickwrap.rb', line 388

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



400
401
402
403
404
405
406
407
408
409
# File 'lib/clickwrap.rb', line 400

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



486
487
488
# File 'lib/clickwrap.rb', line 486

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.



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

def now = Time.now.utc

.policiesObject



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

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:



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

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



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

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



203
204
205
# File 'lib/clickwrap.rb', line 203

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.



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

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

.publish!(dry_run: false) ⇒ Object

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



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

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

.receipt(event_id) ⇒ Object



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

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.



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

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.



265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/clickwrap.rb', line 265

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



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

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.



493
494
495
496
497
498
# File 'lib/clickwrap.rb', line 493

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:



415
416
417
418
419
420
# File 'lib/clickwrap.rb', line 415

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)


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

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.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/clickwrap.rb', line 96

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:



167
168
169
170
171
172
173
174
175
176
# File 'lib/clickwrap.rb', line 167

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



187
188
189
190
191
192
193
# File 'lib/clickwrap.rb', line 187

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

The registry is seeded with one built-in class: evidence kept indefinitely, nothing scheduled for deletion. It exists so a policy that never says retain_with has a real, inspectable retention class instead of a hole — keeping is the reversible default; deletion is the reviewed opt-in. A host wanting deletion clocks declares its own class and names it on the policy. The seed survives every reload (see Registry#clear).



123
124
125
126
127
128
# File 'lib/clickwrap.rb', line 123

def retention_classes
  @retention_classes ||= Registry.new(:retention_class) do |registry|
    registry.register(DEFAULT_RETENTION_CLASS_KEY,
                      RetentionClass.new(key: DEFAULT_RETENTION_CLASS_KEY, rules: {}))
  end
end

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



340
341
342
343
344
345
# File 'lib/clickwrap.rb', line 340

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



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

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



347
348
349
350
351
352
# File 'lib/clickwrap.rb', line 347

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.



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

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.



286
287
288
# File 'lib/clickwrap.rb', line 286

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



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

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



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

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.



304
305
306
307
308
309
# File 'lib/clickwrap.rb', line 304

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