clickwrap 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,1708 @@
1
+ # ☑️ `clickwrap` — trustworthy agreements, consent, declarations, and authorizations for Rails
2
+
3
+ > [!IMPORTANT]
4
+ > **README-first product contract.** `clickwrap` is not implemented or published yet. This README deliberately describes the finished gem we intend to build so we can work backward from the ideal developer experience. Every public promise below is an acceptance criterion, not a claim about code that exists today. Remove this notice only after the implementation and proof integrations satisfy it.
5
+
6
+ `clickwrap` is the missing evidence-and-assent layer for Rails.
7
+
8
+ It makes ordinary Terms acceptance and its action one beautiful form-builder call:
9
+
10
+ ```erb
11
+ <%= form.clickwrap :signup, submit: "Create account" %>
12
+ ```
13
+
14
+ And it grows with you all the way to expiring declarations, withdrawable consent, one-time authorizations, exact historical receipts, transaction-bound evidence, retention, legal holds, and independently verifiable exports—without making the simple path feel complicated.
15
+
16
+ ```ruby
17
+ receipt = Clickwrap.capture_and!(
18
+ :withdrawal_authorization,
19
+ actor: current_user,
20
+ subject: withdrawal,
21
+ http_request: request,
22
+ submission: clickwrap_submission
23
+ ) do |pending_receipt|
24
+ withdrawal.submit!(authorized_by_clickwrap_event: pending_receipt.event_id)
25
+ end
26
+ ```
27
+
28
+ If evidence cannot be recorded, the protected database action does not happen. If the action fails, the evidence does not pretend it succeeded.
29
+
30
+ No JavaScript package. No Redis. No external account. No legal-document vendor. No required per-event API call. No required background job. Just Rails, your database, and an API that reads like plain English.
31
+
32
+ > [!TIP]
33
+ > **Building a new Rails product?** [RailsFast](https://railsfast.com/?ref=clickwrap) ships the conventional signup integration, so new applications start with versioned Terms, a distinct Privacy Notice acknowledgment, atomic evidence, and receipts instead of inventing an `accepted_terms_at` column.
34
+
35
+ ## The five-minute version
36
+
37
+ Install it:
38
+
39
+ ```bash
40
+ bundle add clickwrap
41
+ bin/rails generate clickwrap:install
42
+ bin/rails db:migrate
43
+ ```
44
+
45
+ The installer detects Rails authentication versus Devise, integer versus UUID primary keys, and the database adapter. It generates adaptive migrations, one annotated initializer, a conventional signup policy, and the correct explicit authentication integration. It never invents legal text or silently guesses an ambiguous actor model.
46
+
47
+ Point the generated policy at the exact documents your application already owns:
48
+
49
+ ```ruby
50
+ # config/clickwrap.rb
51
+ Clickwrap.document :terms,
52
+ version: "2026-08-15",
53
+ from: Rails.root.join("app/content/legal/terms.md")
54
+
55
+ Clickwrap.document :privacy_notice,
56
+ version: "2026-08-15",
57
+ from: Rails.root.join("app/content/legal/privacy.md")
58
+
59
+ Clickwrap.policy :signup do
60
+ agree_to :terms
61
+ acknowledge :privacy_notice
62
+ end
63
+ ```
64
+
65
+ Tell Clickwrap which records can act:
66
+
67
+ ```ruby
68
+ # app/models/user.rb
69
+ class User < ApplicationRecord
70
+ has_clickwraps
71
+ end
72
+ ```
73
+
74
+ Render the policy and its bound submit action:
75
+
76
+ ```erb
77
+ <%= form_with model: resource do |form| %>
78
+ <%# email, password, etc. %>
79
+
80
+ <%= form.clickwrap :signup, submit: "Create account" %>
81
+ <% end %>
82
+ ```
83
+
84
+ Publish immutable snapshots and boot the app:
85
+
86
+ ```bash
87
+ bin/rails clickwrap:publish
88
+ ```
89
+
90
+ That is the whole conventional integration. The helper renders the initially unselected controls and the submit button as one presentation, so the exact call to action in the signed manifest is the one the user can press. The generated Rails-authentication or Devise adapter saves the account and required evidence in one database transaction.
91
+
92
+ At first render there is no persisted user yet. Clickwrap does not pretend otherwise: it binds the presentation to a short-lived prospective-actor registration flow, then the authentication adapter binds the resulting account to that presentation inside the same transaction. The receipt identifies the attribution method as account registration, not an authenticated session.
93
+
94
+ From that moment on:
95
+
96
+ ```ruby
97
+ user.clickwraps.agreed_to?(:terms) # => true
98
+ user.clickwraps.acknowledged?(:privacy_notice) # => true
99
+ user.clickwraps.current_for?(:signup) # => true
100
+
101
+ receipt = user.clickwraps.receipts.last
102
+ receipt.event_id # => "01K2..."
103
+ receipt.verify.success? # => true
104
+ receipt.to_canonical_json
105
+ receipt.to_html
106
+ ```
107
+
108
+ Clickwrap preserves the exact document bytes and digests, policy revision, assertion and link text, choices, submit-button text, locale, presentation manifest, actor, authentication context, server time, lifecycle, and resulting protected action. Optional request evidence stays off until you explicitly ask for it.
109
+
110
+ Everything below is depth, not setup tax.
111
+
112
+ If you came for one particular job:
113
+
114
+ - start with [the form helper](#the-form-helper) for ordinary Rails forms;
115
+ - use [`capture_and!`](#capture-evidence-and-the-protected-action-together) for consequential same-database actions;
116
+ - read [consent](#consent-that-can-actually-be-withdrawn), [declarations](#expiring-and-corrected-declarations), or [one-time authorization](#narrow-one-time-authorizations) for richer lifecycles;
117
+ - configure [optional request evidence](#optional-request-evidence-private-by-default) only after reading its privacy boundaries;
118
+ - use [receipts](#receipts-answer-show-me-exactly-what-happened), [retention](#retention-deletion-and-legal-holds-are-first-class), and [integrity tiers](#progressive-honest-integrity) when the audit trail matters; or
119
+ - jump to [the complete initializer](#the-generated-initializer-explains-itself) to see every default together.
120
+
121
+ ---
122
+
123
+ ## Why this gem exists
124
+
125
+ A checkbox is easy. Answering these questions three years later is not:
126
+
127
+ - Which exact version did this person agree to?
128
+ - What did the page actually say beside the control and submit button?
129
+ - Was the checkbox initially empty and required on the server?
130
+ - Did the account, payout, declaration, or provider handoff succeed without its evidence?
131
+ - Was this consent later withdrawn?
132
+ - Had this declaration expired?
133
+ - Did this authorization cover this exact transaction, or was it replayed for another one?
134
+ - Can an auditor reproduce the document without checking out historical application code?
135
+ - Can optional personal request evidence be deleted without rewriting the historical event?
136
+ - Can the exported receipt still be verified after several gem and Rails upgrades?
137
+
138
+ Most applications eventually accumulate some combination of:
139
+
140
+ ```text
141
+ accepted_terms_at
142
+ terms_version
143
+ an audit log
144
+ a few hidden form fields
145
+ an after_create callback
146
+ some IP-address columns
147
+ several domain-specific "confirmed_at" timestamps
148
+ ```
149
+
150
+ Each part looks reasonable alone. Together they produce partial writes, client-owned policy decisions, mutable history, confused consent semantics, and evidence that only the original engineer can explain.
151
+
152
+ `clickwrap` turns that recurring plumbing into one coherent Rails primitive:
153
+
154
+ ```text
155
+ immutable document
156
+ +
157
+ server-owned policy
158
+ +
159
+ exact presentation
160
+ +
161
+ explicit actor action
162
+ +
163
+ atomic protected outcome
164
+ +
165
+ append-only lifecycle
166
+ =
167
+ reproducible receipt
168
+ ```
169
+
170
+ It is intentionally not a “one checkbox makes anything legal” gem. It provides excellent evidence mechanics. Your application and counsel still own the words, lawful basis, fairness, capacity, authority, jurisdiction, formalities, and retention decisions.
171
+
172
+ ## Six verbs, six honest meanings
173
+
174
+ Not every checkbox is “consent,” and not every timestamp is a “signature.” Clickwrap gives each act the lifecycle it actually needs:
175
+
176
+ | Policy verb | Evidence kind | Meaning | Typical lifecycle |
177
+ |---|---|---|---|
178
+ | `agree_to` | `agreement` | Assent to contractual terms | agreed → superseded/new version |
179
+ | `acknowledge` | `acknowledgment` | Affirmative receipt or awareness of a notice/risk | acknowledged → superseded/expired |
180
+ | `consent_to` | `consent` | Purpose-specific permission where consent is the host’s chosen basis | granted → withdrawn/renewed/scope changed |
181
+ | `declare` | `declaration` | A factual statement made by the actor | declared → corrected/superseded/expired |
182
+ | `attest` | `attestation` | An operational fact affirmed by an authorized actor | attested → corrected/superseded |
183
+ | `authorize` | `authorization` | Narrow permission bound to a protected action | authorized → consumed/revoked/expired |
184
+
185
+ The DSL is intentionally verbal:
186
+
187
+ ```ruby
188
+ Clickwrap.policy :example do
189
+ agree_to :terms
190
+ acknowledge :privacy_notice
191
+ consent_to :product_updates, optional: true
192
+ declare :information_is_accurate
193
+ attest :bank_transfer_was_accepted
194
+ authorize :withdrawal, one_time: true, valid_for: 10.minutes
195
+ end
196
+ ```
197
+
198
+ The policy compiler rejects incoherent combinations at boot. A one-time authorization cannot be indefinite. Consent needs a withdrawal path. A declaration can expire without pretending the original statement was false. Withdrawing future consent never rewrites a historical agreement.
199
+
200
+ This taxonomy is product design, not statutory vocabulary. The host chooses the correct kind with appropriate legal/product review.
201
+
202
+ One submitted policy produces one root evidence event and one receipt, even when the policy contains several acts. Each act keeps its own kind, statement, documents, answer, and lifecycle under that root event. That gives the protected domain action one stable `event_id` to reference without flattening “agreed to Terms” and “acknowledged the Privacy Notice” into the same meaning.
203
+
204
+ ## Documents are immutable, reproducible records
205
+
206
+ Define a logical document once and publish as many immutable versions and locales as needed:
207
+
208
+ ```ruby
209
+ Clickwrap.document :terms,
210
+ version: "2026-08-15",
211
+ locale: :en,
212
+ effective_at: Time.utc(2026, 8, 15),
213
+ from: Rails.root.join("app/content/legal/terms.en.md")
214
+
215
+ Clickwrap.document :terms,
216
+ version: "2026-08-15",
217
+ locale: :es,
218
+ effective_at: Time.utc(2026, 8, 15),
219
+ from: Rails.root.join("app/content/legal/terms.es.md")
220
+ ```
221
+
222
+ Publish them during development or deployment:
223
+
224
+ ```bash
225
+ bin/rails clickwrap:publish
226
+ ```
227
+
228
+ Publishing:
229
+
230
+ - reads the exact bytes;
231
+ - records media type and locale;
232
+ - calculates a versioned digest;
233
+ - snapshots the exact rendered representation when a source format is transformed for display;
234
+ - records the renderer and sanitizer identity/version used for that representation;
235
+ - freezes a database snapshot;
236
+ - compiles and freezes every policy revision that references it; and
237
+ - refuses to reuse a version label for different bytes.
238
+
239
+ The task is idempotent. A changed document requires a new version. Export never fetches a mutable live URL and calls it historical evidence.
240
+
241
+ Preview the plan without writing:
242
+
243
+ ```bash
244
+ bin/rails clickwrap:publish:plan
245
+ ```
246
+
247
+ The default database store is deliberately boring and complete. Larger applications can switch document bodies to content-addressed Active Storage or object-lock storage while keeping the same digest and receipt contract:
248
+
249
+ ```ruby
250
+ config.store_document_contents_in = :active_storage
251
+ ```
252
+
253
+ Every storage adapter must return immutable bytes plus a verifiable digest. A URL alone is never a document version.
254
+
255
+ Markdown, HTML, plain text, and attached files are evidence inputs, not trusted markup by accident. The reference renderer sanitizes display HTML. A custom renderer must return the exact rendered bytes it offered, and Clickwrap stores their digest alongside the original-source digest. That preserves the distinction between “this Markdown file existed” and “this rendered representation was offered.”
256
+
257
+ ## Policies are server-owned offers
258
+
259
+ A policy declares what the server will present and accept. The browser may answer; it may never choose the policy, document version, validity, subject, retention, or request-evidence fields.
260
+
261
+ ```ruby
262
+ Clickwrap.policy :driver_declaration do
263
+ declare :non_professional_driver,
264
+ document: :driver_declaration,
265
+ statement: "I declare that I drive privately and not as a professional driver.",
266
+ valid_for: 1.year,
267
+ subject_fingerprint_with: ->(scheme) { scheme.evidence_fingerprint }
268
+
269
+ retain_with :regulated_evidence
270
+ end
271
+ ```
272
+
273
+ Policies compile at boot. Clickwrap fails loudly for:
274
+
275
+ - missing documents or locales;
276
+ - duplicate statement keys;
277
+ - invalid lifecycle options;
278
+ - a consent policy without a configured withdrawal path;
279
+ - a one-time authorization without expiry/consumption behavior;
280
+ - request evidence without a named present purpose and retention decision;
281
+ - a subject-bound policy without a subject fingerprint; or
282
+ - a changed compiled policy reusing the same revision.
283
+
284
+ Policy revisions are defined pleasantly in Ruby and persisted as frozen canonical snapshots. Historical receipts do not need current source code to explain what revision meant.
285
+
286
+ Every human-facing value can be a literal, an I18n key, or a locale map. Clickwrap resolves it before presentation, fails closed when a required translation is missing, and stores the resolved text and locale—not merely an I18n key whose meaning may change later.
287
+
288
+ ### Reacceptance is explicit
289
+
290
+ New document bytes do not silently reinterpret old evidence:
291
+
292
+ ```ruby
293
+ Clickwrap.policy :current_terms do
294
+ agree_to :terms, require_current_version: true
295
+ end
296
+ ```
297
+
298
+ ```ruby
299
+ Clickwrap.required?(:current_terms, actor: user) # => true after a new version publishes
300
+ user.clickwraps.current_for?(:current_terms) # => false
301
+ ```
302
+
303
+ The application decides which change is material. Clickwrap enforces the rule it is given; it does not decide legal materiality.
304
+
305
+ Before activating a new required version, operators can preview its effect:
306
+
307
+ ```bash
308
+ bin/rails clickwrap:reacceptance:plan POLICY=current_terms
309
+ ```
310
+
311
+ The plan reports affected actor counts and configured remediation routes without emailing anyone, changing current state, or calling the change “material.” Scheduled versions become presentable only at their explicit `effective_at`; correcting a published mistake means publishing a new version or stopping future presentation with an append-only operator reason, never replacing historical bytes.
312
+
313
+ ## Presentation manifests stop render-to-submit substitution
314
+
315
+ `form.clickwrap` does more than render controls. It creates a short-lived presentation manifest bound to an actor or prospective-actor flow, subject, and tenant containing:
316
+
317
+ - policy key and frozen revision;
318
+ - document versions, locales, and digests;
319
+ - exact statements, labels, link labels/targets, choices, required state, and CTA text;
320
+ - actor, tenant, and subject bindings;
321
+ - subject fingerprint;
322
+ - template, application, and gem versions;
323
+ - capture channel;
324
+ - issue time, expiry, and one-use nonce; and
325
+ - a canonical manifest digest.
326
+
327
+ The browser receives a signed presentation token. On submit, Clickwrap verifies it against current server policy and rejects stale, swapped, expired, cross-account, cross-tenant, or cross-subject tokens.
328
+
329
+ A deploy between GET and POST never causes the server to record a version the actor was not offered. The policy either honors that still-valid presentation or asks the user to review the new one.
330
+
331
+ The default signed-manifest path performs no database write on GET. A high-assurance flow can explicitly retain pre-submit presentation attempts:
332
+
333
+ ```ruby
334
+ Clickwrap.policy :regulated_authorization do
335
+ persist_presentations_before_submission_for 30.days,
336
+ because: "Investigate disputes about this regulated authorization"
337
+ authorize :regulated_action, one_time: true, valid_for: 10.minutes
338
+ end
339
+ ```
340
+
341
+ Persisted presentations carry their own purpose, access, abuse controls, and retention; an abandoned GET is labeled `presented_by_server`, never `accepted` or `seen_by_human`.
342
+
343
+ The receipt says exactly what this proves: the server generated and accepted a particular presentation manifest. It does not claim the person read the document, understood it, saw particular pixels, or received a legally sufficient interface in every jurisdiction.
344
+
345
+ ### The form helper
346
+
347
+ The strongest happy path is one line because the component owns both the controls and the action whose wording it records:
348
+
349
+ ```erb
350
+ <%= form.clickwrap :signup, submit: "Create account" %>
351
+ ```
352
+
353
+ Submit options remain ordinary Rails:
354
+
355
+ ```erb
356
+ <%= form.clickwrap :signup,
357
+ actor: current_user,
358
+ subject: @organization,
359
+ locale: I18n.locale,
360
+ submit: {
361
+ text: "Create organization",
362
+ class: "button button--primary",
363
+ data: { turbo_submits_with: "Creating…" }
364
+ } %>
365
+ ```
366
+
367
+ The helper renders:
368
+
369
+ - real, initially unselected controls;
370
+ - kind-appropriate first-person language;
371
+ - obvious document links before the submit action;
372
+ - stable label/control/error associations;
373
+ - server errors and accessible error summaries;
374
+ - the signed presentation token; and
375
+ - no hidden IP address, browser user-agent, policy version, validity date, or other client-owned security decision.
376
+
377
+ HTML `required` is progressive enhancement. Server validation is always authoritative.
378
+
379
+ If your design system needs to render the action separately, use the deliberately explicit split API:
380
+
381
+ ```erb
382
+ <%= form.clickwrap_fields :signup,
383
+ submit_button_text: "Create account" %>
384
+
385
+ <%= form.submit "Create account" %>
386
+ ```
387
+
388
+ The repeated text is intentional: it makes the evidence contract visible in code. Development and system-test assertions compare the declared text with the rendered submit control and reject a mismatch. The one-call API is preferred because it makes that class of drift impossible.
389
+
390
+ ### Use the ready-made standalone remediation screen
391
+
392
+ Any policy can be completed outside its original flow:
393
+
394
+ ```ruby
395
+ # config/routes.rb
396
+ mount Clickwrap::Engine => "/agreements"
397
+ ```
398
+
399
+ ```ruby
400
+ clickwrap_capture_path(:driver_declaration)
401
+ ```
402
+
403
+ The engine provides actor-owned capture, receipt, consent-withdrawal, and document-history surfaces using your parent controller, layout, locale, and authorization callbacks. This makes a required agreement or declaration resolvable in place instead of becoming a dead end.
404
+
405
+ ### Eject or fully own the UI
406
+
407
+ Copy the tested reference views:
408
+
409
+ ```bash
410
+ bin/rails generate clickwrap:views
411
+ ```
412
+
413
+ Your copies shadow the gem’s views. Tailwind, Bootstrap, ViewComponent, Phlex, custom design systems, and plain ERB are all welcome.
414
+
415
+ For a completely custom surface, ask the presenter for primitives rather than recreating hidden inputs:
416
+
417
+ ```ruby
418
+ presentation = Clickwrap.present(
419
+ :signup,
420
+ actor: current_user,
421
+ subject: nil,
422
+ locale: I18n.locale,
423
+ submit_button_text: "Create account"
424
+ )
425
+ ```
426
+
427
+ ```erb
428
+ <%= hidden_field_tag "clickwrap_submission[presentation_token]", presentation.token %>
429
+
430
+ <% presentation.statements.each do |statement| %>
431
+ <%# Render statement.control_name, label, document links, choices and errors. %>
432
+ <% end %>
433
+ ```
434
+
435
+ The development linter compares the submitted manifest with the policy/presenter contract and warns about missing statements, preselected consent, absent links, controls placed after the CTA, or unregistered custom copy. It reports objective problems; it never prints “legally compliant.”
436
+
437
+ ## Capture evidence and the protected action together
438
+
439
+ For an existing actor in a normal Rails controller:
440
+
441
+ ```ruby
442
+ def create
443
+ withdrawal = current_user.withdrawals.build(withdrawal_params)
444
+
445
+ receipt = capture_clickwrap_and!(
446
+ :withdrawal_authorization,
447
+ actor: current_user,
448
+ subject: withdrawal
449
+ ) do |pending_receipt|
450
+ withdrawal.submit!(authorized_by_clickwrap_event: pending_receipt.event_id)
451
+ end
452
+
453
+ redirect_to withdrawal
454
+ end
455
+ ```
456
+
457
+ The controller helper reads only the generated `clickwrap_submission` envelope and the current `http_request`. It delegates to the same public service API:
458
+
459
+ ```ruby
460
+ receipt = Clickwrap.capture_and!(
461
+ :withdrawal_authorization,
462
+ actor: current_user,
463
+ subject: withdrawal,
464
+ http_request: request,
465
+ submission: clickwrap_submission
466
+ ) do |pending_receipt|
467
+ withdrawal.submit!(authorized_by_clickwrap_event: pending_receipt.event_id)
468
+ end
469
+ ```
470
+
471
+ Within one supported database transaction, Clickwrap:
472
+
473
+ 1. verifies actor, tenant, subject, presentation, policy, document digests, answers, expiry, and nonce;
474
+ 2. acquires the required idempotency/subject locks;
475
+ 3. appends the pending evidence event;
476
+ 4. yields its receipt to the protected domain action;
477
+ 5. records the resulting outcome and consumes one-time authorization where applicable;
478
+ 6. commits both together; and
479
+ 7. invokes optional notifications/analytics only after commit.
480
+
481
+ If the event write fails, the protected action rolls back. If the block raises, the event rolls back. Repeating an identical idempotency key returns the original result without running the block twice. A conflicting replay fails with a stable `Clickwrap::ReplayRejected` result.
482
+
483
+ The block receives a read-only `Clickwrap::PendingReceipt`. Its stable `event_id` can be stored by the domain row, but export/verification methods are unavailable until commit. `capture_and!` returns the finalized `Clickwrap::Receipt`; if the transaction rolls back, the pending object becomes invalid instead of masquerading as committed evidence.
484
+
485
+ Atomic commit does not give Clickwrap permission to guess what a host method meant. Without a configured outcome snapshot, the receipt says only that the named policy, bound subject, evidence event, and block committed together. `record_protected_outcome_with` can add an exact post-action reference/state/fingerprint; it runs and validates inside the transaction, and a failure rolls the whole operation back.
486
+
487
+ The transaction contract is documented precisely for ownership, nested transactions, savepoints, deadlock/serialization retries, idempotency, callbacks, and after-commit behavior. Automatic retries occur only when Clickwrap can prove the block is safe to retry; otherwise a stable retryable error returns control to the host. Clickwrap never promises atomicity across two independent systems.
488
+
489
+ ### Capture without a protected action
490
+
491
+ ```ruby
492
+ receipt = Clickwrap.capture!(
493
+ :current_terms,
494
+ actor: current_user,
495
+ http_request: request,
496
+ submission: clickwrap_submission
497
+ )
498
+ ```
499
+
500
+ ### Devise and Rails authentication
501
+
502
+ The installer detects the authentication stack and generates an explicit adapter—not a hidden `after_create` callback.
503
+
504
+ For Devise, the generated controller reads:
505
+
506
+ ```ruby
507
+ class Users::RegistrationsController < Devise::RegistrationsController
508
+ clickwraps_registration_with :signup
509
+ end
510
+ ```
511
+
512
+ For Rails’ authentication generator, the generated registration command uses:
513
+
514
+ ```ruby
515
+ register_with_clickwrap :signup, user: @user do
516
+ @user.save!
517
+ end
518
+ ```
519
+
520
+ Both integrations ensure account activation and required evidence commit together. Emails, sign-in, redirects, and after-commit side effects occur only after the transaction has succeeded. A failed evidence write never leaves a normal public account silently active.
521
+
522
+ Signup is modeled honestly as a prospective-actor flow:
523
+
524
+ 1. the GET creates a short-lived, signed registration-flow identifier;
525
+ 2. the presentation token binds to that flow, the form object type, and any host-selected tenant—not to a fictional persisted or authenticated user;
526
+ 3. the adapter validates the submitted presentation before account activation;
527
+ 4. one transaction persists the account, binds its stable actor reference to the evidence, and commits both; and
528
+ 5. the receipt records `account_registration` attribution and the actual pre-registration authentication state.
529
+
530
+ Email addresses, passwords, and raw signup fields are not copied into the token. A token from another browser flow, tenant, form object, or already-created account is rejected. Applications that own a custom registration service use the same primitive directly:
531
+
532
+ ```ruby
533
+ receipt = Clickwrap.register!(
534
+ :signup,
535
+ prospective_actor: @user,
536
+ http_request: request,
537
+ submission: clickwrap_submission
538
+ ) do
539
+ @user.save!
540
+ end
541
+ ```
542
+
543
+ `register!` returns the same receipt type as `capture_and!`; the authentication adapters are thin conveniences over it.
544
+
545
+ ### External providers use an outbox, not pretend-ACID
546
+
547
+ Stripe, identity services, timestamp providers, and remote signatures cannot share your database transaction. Use a pending authorization and idempotent outbox:
548
+
549
+ ```ruby
550
+ authorization = Clickwrap.authorize_external_action!(
551
+ :identity_provider_handoff,
552
+ actor: current_user,
553
+ subject: verification,
554
+ http_request: request,
555
+ submission: clickwrap_submission
556
+ )
557
+
558
+ ProviderHandoffJob.perform_later(
559
+ authorization_id: authorization.id,
560
+ idempotency_key: authorization.idempotency_key
561
+ )
562
+ ```
563
+
564
+ ```ruby
565
+ authorization.record_provider_success_and_consume!(provider_receipt)
566
+ ```
567
+
568
+ That final method is one idempotent local transaction. Failures and ambiguous timeouts use `record_provider_failure!` and `record_provider_outcome_unknown!`; the reconciliation task can safely resolve them later. A provider timeout never becomes a fictional success or a second debit.
569
+
570
+ ## Ask readable questions everywhere
571
+
572
+ The actor proxy is the everyday API:
573
+
574
+ ```ruby
575
+ user.clickwraps.current_for?(:signup)
576
+ user.clickwraps.required_for?(:current_terms)
577
+ user.clickwraps.agreed_to?(:terms)
578
+ user.clickwraps.acknowledged?(:privacy_notice)
579
+ user.clickwraps.consented_to?(:product_updates)
580
+ user.clickwraps.declared?(:non_professional_driver, subject: scheme)
581
+ user.clickwraps.authorized?(:withdrawal, subject: withdrawal)
582
+ ```
583
+
584
+ Every predicate has a structured form when “no” needs an explanation:
585
+
586
+ ```ruby
587
+ result = Clickwrap.verify(
588
+ :withdrawal_authorization,
589
+ actor: user,
590
+ subject: withdrawal
591
+ )
592
+
593
+ result.success? # => false
594
+ result.error # => :declaration_expired
595
+ result.message # localized human explanation
596
+ result.event_id
597
+ result.details # stable machine-readable facts, no surprise PII
598
+ ```
599
+
600
+ Stable errors cover wrong actor/tenant/subject, stale policy, unseen document version, missing answer, expiry, withdrawal, predecessor/order, fingerprint mismatch, consumption, replay, and integrity failure.
601
+
602
+ The convention is consistent: predicates answer booleans, `verify` returns a result, and bang methods raise a typed error carrying that same result. Applications never need to parse an English error message to make an authorization decision.
603
+
604
+ ### Controller gates that always have remediation
605
+
606
+ ```ruby
607
+ class BillingController < ApplicationController
608
+ requires_clickwrap :current_terms, only: :show
609
+ end
610
+ ```
611
+
612
+ The gate redirects HTML/Hotwire users to the mounted policy capture screen and returns them to the original safe destination after completion. API clients receive a structured `clickwrap_required` response with a presentation endpoint.
613
+
614
+ A required gate must have a remediation route or an explicit host support fallback. Clickwrap refuses to compile a dead-end gate.
615
+
616
+ Security-sensitive services should still verify at the domain boundary:
617
+
618
+ ```ruby
619
+ Clickwrap.require!(
620
+ :withdrawal_authorization,
621
+ actor: user,
622
+ subject: withdrawal
623
+ )
624
+ ```
625
+
626
+ Controller gates improve flow; service verification protects the action.
627
+
628
+ ## Consent that can actually be withdrawn
629
+
630
+ Consent is purpose-specific, initially unselected, and separate from Terms or a Privacy Notice acknowledgment:
631
+
632
+ ```ruby
633
+ Clickwrap.document :marketing_notice,
634
+ version: "2026-08-15",
635
+ from: Rails.root.join("app/content/legal/marketing.md")
636
+
637
+ Clickwrap.policy :marketing_preferences do
638
+ consent_to :product_updates,
639
+ document: :marketing_notice,
640
+ optional: true,
641
+ withdrawal_path: "/settings/privacy"
642
+
643
+ consent_to :partner_offers,
644
+ document: :marketing_notice,
645
+ optional: true,
646
+ withdrawal_path: "/settings/privacy"
647
+
648
+ retain_with :marketing_consent_evidence
649
+ end
650
+ ```
651
+
652
+ Leaving an optional checkbox unselected creates no consent grant. The capture receipt can show that the option was offered and not granted, but it does not call silence an affirmative refusal. A policy that truly needs a recorded yes/no choice uses explicit unselected controls:
653
+
654
+ ```ruby
655
+ consent_to :research_contact,
656
+ choices: { yes: :grant, no: :decline },
657
+ require_an_explicit_choice: true,
658
+ withdrawal_path: "/settings/privacy"
659
+ ```
660
+
661
+ ```ruby
662
+ Clickwrap.withdraw!(
663
+ :product_updates,
664
+ actor: current_user,
665
+ http_request: request,
666
+ because: "The user withdrew this purpose in privacy settings"
667
+ )
668
+ ```
669
+
670
+ Withdrawal appends an event; it never deletes or mutates the historical grant. The policy’s post-commit hook can stop future processing or enqueue host-owned deletion work without making the original transaction depend on an analytics/job backend.
671
+
672
+ ```ruby
673
+ config.after_event_is_committed = lambda do |event|
674
+ Marketing::StopProcessingJob.perform_later(event.actor_id) if event.consent_was_withdrawn?
675
+ end
676
+ ```
677
+
678
+ Clickwrap structurally requires an accessible withdrawal path. It does not decide whether consent is the correct lawful basis.
679
+
680
+ ## Expiring and corrected declarations
681
+
682
+ ```ruby
683
+ Clickwrap.policy :driver_declaration do
684
+ declare :non_professional_driver,
685
+ document: :driver_declaration,
686
+ valid_for: 1.year,
687
+ subject_fingerprint_with: ->(scheme) { scheme.evidence_fingerprint }
688
+ end
689
+ ```
690
+
691
+ ```ruby
692
+ user.clickwraps.declared?(:non_professional_driver, subject: scheme)
693
+ user.clickwraps.declaration(:non_professional_driver, subject: scheme).expires_at
694
+ ```
695
+
696
+ Renewal always starts a new validity period. Correction, supersession, and expiry append linked lifecycle events:
697
+
698
+ ```ruby
699
+ Clickwrap.correct_declaration!(
700
+ :non_professional_driver,
701
+ actor: user,
702
+ subject: scheme,
703
+ replaces: old_receipt,
704
+ http_request: request,
705
+ submission: clickwrap_submission
706
+ )
707
+ ```
708
+
709
+ The host retains domain-specific eligibility and declaration models. Clickwrap owns presentation, evidence, lifecycle, receipts, and verification—not your business rules.
710
+
711
+ ## Narrow, one-time authorizations
712
+
713
+ ```ruby
714
+ Clickwrap.policy :withdrawal_authorization do
715
+ acknowledge :withdrawal_requirements
716
+
717
+ declare :ride_exclusivity,
718
+ subject_fingerprint_with: ->(withdrawal) { withdrawal.covered_rides_fingerprint }
719
+
720
+ authorize :withdrawal,
721
+ one_time: true,
722
+ valid_for: 10.minutes,
723
+ requires: %i[withdrawal_requirements ride_exclusivity],
724
+ record_protected_outcome_with: lambda { |withdrawal|
725
+ {
726
+ action: :submitted,
727
+ reference: withdrawal.to_gid.to_s,
728
+ fingerprint: withdrawal.evidence_fingerprint
729
+ }
730
+ }
731
+ end
732
+ ```
733
+
734
+ `capture_and!` locks and consumes the authorization in the same transaction as the withdrawal. Another withdrawal, changed ride set, stale declaration, wrong ordering, or concurrent replay cannot reuse it.
735
+
736
+ This is the core difference between “the user once accepted something” and “this exact evidence authorized this exact operation.”
737
+
738
+ ## Operator attestations
739
+
740
+ ```ruby
741
+ Clickwrap.policy :manual_bank_transfer do
742
+ attest :beneficiary_matches_verified_identity
743
+ attest :bank_accepted_transfer
744
+ authorize :record_transfer_as_sent, one_time: true
745
+ end
746
+ ```
747
+
748
+ Attestations preserve which authorized operator asserted which operational fact, under which role and authentication context, while the host owns permissions and domain state.
749
+
750
+ ## External agreements and imported receipts
751
+
752
+ When Stripe, DocuSign, Ironclad, or another provider owns the presentation, do not pretend your application captured the click:
753
+
754
+ ```ruby
755
+ Clickwrap.import_external_receipt!(
756
+ :connected_account_service_agreement,
757
+ actor: user,
758
+ provider_name: "stripe",
759
+ provider_event_id: account.id,
760
+ provider_receipt: account.service_agreement,
761
+ verified_with: :stripe_api,
762
+ verified_at: Time.current
763
+ )
764
+ ```
765
+
766
+ The event is labeled `external_receipt`, preserves provider provenance and validation status, and can participate in host verification without becoming a fictional local presentation.
767
+
768
+ ## Receipts answer “show me exactly what happened”
769
+
770
+ Every event has one canonical JSON receipt and one human-readable HTML projection:
771
+
772
+ ```ruby
773
+ receipt = Clickwrap.receipt(event_id)
774
+
775
+ receipt.to_canonical_json
776
+ receipt.to_html
777
+ receipt.to_pdf # optional renderer; never the source of truth
778
+ receipt.verify
779
+ ```
780
+
781
+ An abbreviated receipt looks like:
782
+
783
+ ```json
784
+ {
785
+ "schema": "clickwrap.receipt.v1",
786
+ "event_id": "01K2Y8T5QY0N4V6N1H4G4CQY8J",
787
+ "policy": { "key": "signup", "revision": "sha256:..." },
788
+ "actor": {
789
+ "type": "User",
790
+ "reference": "usr_...",
791
+ "attribution": { "method": "account_registration", "authenticated": false }
792
+ },
793
+ "acts": [
794
+ { "statement": "terms", "kind": "agreement", "action": "agreed" },
795
+ {
796
+ "statement": "privacy_notice",
797
+ "kind": "acknowledgment",
798
+ "action": "acknowledged"
799
+ }
800
+ ],
801
+ "documents": [
802
+ { "key": "terms", "version": "2026-08-15", "locale": "en", "sha256": "..." },
803
+ {
804
+ "key": "privacy_notice",
805
+ "version": "2026-08-15",
806
+ "locale": "en",
807
+ "sha256": "..."
808
+ }
809
+ ],
810
+ "presentation": {
811
+ "manifest_sha256": "...",
812
+ "submit_button_text": "Create account",
813
+ "offered_at": "2026-08-15T12:34:56.123456Z"
814
+ },
815
+ "outcome": { "type": "User", "reference": "usr_...", "status": "created" },
816
+ "request_evidence": {
817
+ "ip_address": { "state": "not_configured" },
818
+ "browser_user_agent": { "state": "not_configured" },
819
+ "ip_geolocation": { "state": "not_configured" }
820
+ },
821
+ "integrity": { "digest_algorithm": "sha256", "verified": true }
822
+ }
823
+ ```
824
+
825
+ The bundle can include exact document files, manifest, per-act lifecycle/predecessor graph, protected outcome, optional provider receipts, integrity/checkpoint verification, system explanation, and verifier version.
826
+
827
+ `to_canonical_json` returns the verifiable core receipt and omits raw sensitive request evidence by default. Raw IP address, browser user-agent, and IP-geolocation values live in a separately encrypted evidence annex with its own digest, authorization, retention, hold, and disposition state. That boundary lets the core event remain immutable when a permitted retention process later removes the annex.
828
+
829
+ Canonical receipts use versioned schemas and the [JSON Canonicalization Scheme (RFC 8785)](https://www.rfc-editor.org/rfc/rfc8785), plus a published Clickwrap profile for UTC timestamps, decimals, identifiers, binary digests, absent values, and extension names. They never depend on Ruby object serialization, YAML, database column order, or the current policy source. Unknown schema versions fail honestly instead of being “best effort” reinterpreted.
830
+
831
+ ### View and download
832
+
833
+ With the engine mounted:
834
+
835
+ ```ruby
836
+ clickwrap_receipt_path(receipt)
837
+ ```
838
+
839
+ Actors can view their own receipts. Operator access is always host-authorized:
840
+
841
+ ```ruby
842
+ config.authorize_receipt_access_with = lambda do |controller, receipt|
843
+ controller.current_user == receipt.actor || controller.current_user.admin?
844
+ end
845
+ ```
846
+
847
+ Foreign IDs return not found; existence is not leaked.
848
+
849
+ ### Export only the sensitive fields you intend
850
+
851
+ ```ruby
852
+ Clickwrap.export_receipt(
853
+ receipt,
854
+ requested_by: current_operator,
855
+ because: "Investigate dispute 2026-184",
856
+ include_ip_address: false,
857
+ include_browser_user_agent: false,
858
+ include_ip_geolocation: false
859
+ )
860
+ ```
861
+
862
+ There is intentionally no vague `include_sensitive_context: true` switch. Unredacted operator access and export require host authorization plus a human-readable reason and append an access event. Actor self-service follows the host’s configured disclosure policy without revealing internal fraud/security fields by accident.
863
+
864
+ ### Verify inside or outside the application
865
+
866
+ ```ruby
867
+ Clickwrap::Receipt.verify(canonical_json, documents: document_files)
868
+ ```
869
+
870
+ ```bash
871
+ clickwrap verify receipt.json --documents ./receipt-documents
872
+ ```
873
+
874
+ The standalone verifier does not need the host application’s source code. At the baseline tier it verifies schema, canonical bytes, digests, links, and bundled content consistency; it does not claim that a self-contained file could not have been fabricated by someone controlling every source. Independent anchors/provider signatures add the stronger origin/time evidence they actually supply. Golden fixtures ensure new releases continue verifying every historical receipt format.
875
+
876
+ ## Optional request evidence, private by default
877
+
878
+ Clickwrap always records its event ID, server time, capture channel, policy/application version, configured actor/authentication source, and HTTP request ID when available.
879
+
880
+ It records none of these personal/request-derived fields unless the initializer or policy names them:
881
+
882
+ - raw IP address;
883
+ - raw browser User-Agent;
884
+ - IP-geolocation country, region, city, postal code, coordinates, timezone, continent, metro code, or accuracy radius;
885
+ - browser/device fingerprints; or
886
+ - actual GPS/device location.
887
+
888
+ Browser fingerprinting and GPS are never collected by the base gem. IP geolocation is provider-estimated network context—not identity, GPS, a street address, or proof that the person was physically there.
889
+
890
+ Those defaults are evidence design, not fear of useful data. IP addresses and linked online identifiers can be personal data ([Breyer, C-582/14](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A62014CJ0582)); keeping them on first-party infrastructure does not remove purpose, lawful-basis, transparency, minimization, protection-by-default, security, retention, or high-risk-assessment duties ([GDPR Articles 5](https://eur-lex.europa.eu/eli/reg/2016/679/art_5/oj/eng), [6](https://eur-lex.europa.eu/eli/reg/2016/679/art_6/oj/eng), [13](https://eur-lex.europa.eu/eli/reg/2016/679/art_13/oj/eng), [25](https://eur-lex.europa.eu/eli/reg/2016/679/art_25/oj/eng), [32](https://eur-lex.europa.eu/eli/reg/2016/679/art_32/oj/eng), and [35](https://eur-lex.europa.eu/eli/reg/2016/679/art_35/oj/eng)). Clickwrap therefore supports rich capture while requiring a present, named posture.
891
+
892
+ MaxMind expressly describes GeoIP as approximate and not capable of identifying a household, individual, or street address; Cloudflare describes its fields as location information for an IP address ([MaxMind accuracy guidance](https://support.maxmind.com/knowledge-base/articles/maxmind-geolocation-accuracy); [Cloudflare IP geolocation](https://developers.cloudflare.com/network/ip-geolocation/)). Clickwrap preserves that uncertainty instead of polishing an estimate into a stronger claim.
893
+
894
+ ### Enable exactly what one policy needs
895
+
896
+ ```ruby
897
+ Clickwrap.policy :regulated_authorization do
898
+ authorize :regulated_action, one_time: true, valid_for: 10.minutes
899
+
900
+ review_request_evidence_configuration_on Date.new(2027, 8, 15)
901
+
902
+ record_ip_address(
903
+ encrypted: true,
904
+ retain_until: :regulated_evidence_retention_ends,
905
+ because: "Investigate account compromise and disputes about this action",
906
+ legal_basis_reference: "LIA-SECURITY-2026-01"
907
+ )
908
+
909
+ record_browser_user_agent(
910
+ encrypted: true,
911
+ retain_until: :regulated_evidence_retention_ends,
912
+ because: "Corroborate the client context used for this action",
913
+ legal_basis_reference: "LIA-SECURITY-2026-01"
914
+ )
915
+
916
+ record_ip_geolocation(
917
+ country: true,
918
+ region: true,
919
+ city: true,
920
+ postal_code: false,
921
+ latitude_and_longitude: true,
922
+ timezone: true,
923
+ continent: false,
924
+ metro_code: false,
925
+ accuracy_radius_in_kilometers: true,
926
+ using: :trackdown,
927
+ retain_until: :regulated_evidence_retention_ends,
928
+ because: "Corroborate anomalous access and investigate action disputes",
929
+ legal_basis_reference: "LIA-SECURITY-2026-01",
930
+ data_protection_impact_assessment_reference: "DPIA-2026-04"
931
+ )
932
+ end
933
+ ```
934
+
935
+ Every enabled IP-geolocation result carries provider name/source, estimated state, resolution time, unavailable reason, and any database/accuracy provenance the resolver supplies. A policy cannot keep provider-derived coordinates while stripping the uncertainty needed to interpret them.
936
+
937
+ Receipts distinguish `not_configured`, `unavailable`, `recorded`, `redacted_for_this_viewer`, `deleted_after_retention`, and `held`. “Blank” is never allowed to blur “we chose not to collect it” into “collection failed.”
938
+
939
+ The browser cannot submit or replace server-observed values. Clickwrap conventionally reads `request.remote_ip`, and the host must configure/test trusted proxies correctly; Rails documents the forwarding, trusted-proxy, and spoof-check assumptions in [`ActionDispatch::RemoteIp`](https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html).
940
+
941
+ Required request enrichment resolves before the evidence/domain transaction begins and is carried into it as verified input; it is never filled in later by analytics. A policy chooses explicitly whether an unavailable resolver blocks capture or produces an `unavailable` state. Network resolvers are supported, but local databases or already-verified edge metadata avoid holding a domain transaction open around a remote call.
942
+
943
+ ### Trackdown is the optional official resolver
944
+
945
+ ```ruby
946
+ bundle add trackdown
947
+ ```
948
+
949
+ ```ruby
950
+ config.ip_geolocation_resolver =
951
+ Clickwrap::IpGeolocation::TrackdownResolver.new
952
+ ```
953
+
954
+ `trackdown` remains optional. Clickwrap stores only the fields authorized by the active server policy, never the entire result object. Provider presence is not source trust: Cloudflare-derived fields are marked host-verified only when the application explicitly verifies that requests came through its trusted Cloudflare path.
955
+
956
+ `footprinted` remains analytics, not authoritative evidence. A sanitized event ID/policy/kind may be emitted to analytics after commit; analytics failure can never undo or substitute for the Clickwrap event.
957
+
958
+ ### Easy installer recipes without a fake compliance switch
959
+
960
+ The installer can scaffold either starting point:
961
+
962
+ ```bash
963
+ bin/rails generate clickwrap:install \
964
+ --request-evidence-recipe=privacy-minimized
965
+ ```
966
+
967
+ ```bash
968
+ bin/rails generate clickwrap:install \
969
+ --request-evidence-recipe=evidence-rich
970
+ ```
971
+
972
+ The second recipe asks about every field, purpose, encryption choice, access/export policy, trusted-source posture, and retention rule. It then writes every individual setting into the initializer and disappears. There is no runtime `gdpr_compliant_mode`, `maximum_evidence`, `track_everything`, or `legal_proof` option.
973
+
974
+ Recipes are scaffolding, never compliance verdicts.
975
+
976
+ ## Retention, deletion, and legal holds are first-class
977
+
978
+ Every policy chooses an application-defined retention class:
979
+
980
+ ```ruby
981
+ Clickwrap.retention :ordinary_agreement_evidence do
982
+ retain_core_event_for 6.years
983
+ delete_recorded_ip_address_after 90.days
984
+ delete_recorded_browser_user_agent_after 90.days
985
+ delete_recorded_ip_geolocation_after 90.days
986
+ end
987
+ ```
988
+
989
+ Event-based and “later of” rules are supported for regulated records:
990
+
991
+ ```ruby
992
+ Clickwrap.retention :regulated_evidence do
993
+ retain_core_event_until :regulated_evidence_retention_ends
994
+ retain_recorded_ip_address_until :security_evidence_retention_ends
995
+ retain_recorded_browser_user_agent_until :security_evidence_retention_ends
996
+ retain_recorded_ip_geolocation_until :security_evidence_retention_ends
997
+ end
998
+ ```
999
+
1000
+ ```ruby
1001
+ config.calculate_retention_time_for :regulated_evidence_retention_ends do |event|
1002
+ [
1003
+ event.recorded_at_by_server + 5.years,
1004
+ event.subject_liquidated_at&.+(3.years)
1005
+ ].compact.max
1006
+ end
1007
+ ```
1008
+
1009
+ Clickwrap does not decide those periods. It makes reviewed policies executable and auditable.
1010
+
1011
+ Preview every disposition before applying it:
1012
+
1013
+ ```bash
1014
+ bin/rails clickwrap:retention:plan
1015
+ bin/rails clickwrap:retention:apply PLAN=01K2Y8T5QY0N4V6N1H4G4CQY8J
1016
+ ```
1017
+
1018
+ The plan is immutable, scoped, expiring, and rechecked at apply time. A newly placed hold, changed policy, changed eligibility, or stale plan stops disposition instead of deleting a broader set than the operator reviewed.
1019
+
1020
+ Destructive public methods name exactly what they remove:
1021
+
1022
+ ```ruby
1023
+ Clickwrap.delete_recorded_ip_address!(receipt, because: "Retention period ended")
1024
+ Clickwrap.delete_recorded_browser_user_agent!(receipt, because: "Retention period ended")
1025
+ Clickwrap.delete_recorded_ip_geolocation!(receipt, because: "Retention period ended")
1026
+ ```
1027
+
1028
+ Deletion removes the selected encrypted annex value, appends a disposition event, and changes the current receipt projection to `deleted`; it does not rewrite the historical agreement/declaration/authorization. Verification thereafter proves the immutable core event and its disposition history while reporting that the raw annex value is no longer available. A retained digest is described as a retained linkable digest, never automatically called anonymous.
1029
+
1030
+ ### Legal holds
1031
+
1032
+ ```ruby
1033
+ receipt.place_on_legal_hold!(
1034
+ because: "Pending dispute 2026-184",
1035
+ placed_by: current_operator,
1036
+ review_on: 6.months.from_now
1037
+ )
1038
+
1039
+ receipt.release_legal_hold!(
1040
+ because: "Dispute resolved",
1041
+ released_by: current_operator
1042
+ )
1043
+ ```
1044
+
1045
+ A hold pauses scheduled disposition, requires a reason/owner/review date, and is itself append-only evidence.
1046
+
1047
+ Deleting an actor account never silently cascades evidence. The installer uses restrictive/nullifying relationships plus a stable configured pseudonymous actor reference. Host retention policy decides what remains.
1048
+
1049
+ ### Privacy inventory and actor requests
1050
+
1051
+ Clickwrap can describe what the application configured without pretending that configuration is lawful:
1052
+
1053
+ ```bash
1054
+ bin/rails clickwrap:privacy:inventory
1055
+ bin/rails clickwrap:privacy:export ACTOR=gid://my-app/User/123
1056
+ bin/rails clickwrap:privacy:disposition:plan ACTOR=gid://my-app/User/123
1057
+ ```
1058
+
1059
+ The inventory lists every policy, personal/request-derived field, stated purpose, host-supplied legal-basis reference, provider/source, encryption state, access callback, retention rule, unresolved host event, and review date. The actor export uses the same authorization/redaction rules as receipts. The disposition command only creates a reviewable plan; it does not decide whether an erasure request overrides retention duties, legal claims, or a hold.
1060
+
1061
+ Programmatic equivalents return structured results for a host-owned privacy workflow:
1062
+
1063
+ ```ruby
1064
+ Clickwrap::Privacy.inventory
1065
+ Clickwrap::Privacy.export_for(actor, requested_by: current_operator)
1066
+ Clickwrap::Privacy.plan_disposition_for(
1067
+ actor,
1068
+ requested_by: current_operator,
1069
+ because: "Verified erasure request DSAR-2026-41"
1070
+ )
1071
+ ```
1072
+
1073
+ Correcting an actor’s current email/name or unlinking an account changes the host projection, not the historical snapshot. A host may append a correction/linkage event when needed; Clickwrap never silently edits what an old receipt recorded.
1074
+
1075
+ ## Progressive, honest integrity
1076
+
1077
+ Clickwrap starts useful with an ordinary Rails database and lets serious applications add assurance without changing the capture API.
1078
+
1079
+ | Tier | Capability | Honest claim |
1080
+ |---|---|---|
1081
+ | Baseline | Canonical receipts, immutable snapshots, versioned SHA-256 digests, append-only public API, independent verifier | Detects accidental/ordinary mutation of the verified bytes |
1082
+ | Database hardening | Constraints and adapter-specific update/delete protections | Rejects unsupported mutation paths within the documented database threat model |
1083
+ | Chained history | Per-tenant or per-aggregate event chains/checkpoints | Makes rewriting history detectable when checkpoints remain trustworthy |
1084
+ | Independent anchoring | Heads stored/published outside the primary database | Improves evidence against a privileged primary-database rewrite |
1085
+ | Trusted timestamp/provider | RFC 3161 or qualified trust-service receipt adapters | Preserves exactly the assurance and validation status supplied by that provider |
1086
+
1087
+ Enable optional hardening explicitly:
1088
+
1089
+ ```bash
1090
+ bin/rails generate clickwrap:hardening --database
1091
+ bin/rails db:migrate
1092
+ ```
1093
+
1094
+ ```ruby
1095
+ config.digest_canonical_receipts_with = :sha256
1096
+ config.chain_event_history_with = :sha256
1097
+ config.anchor_event_history_with = MyIndependentAnchor.new
1098
+ config.timestamp_receipts_with = MyRfc3161TimestampProvider.new
1099
+ ```
1100
+
1101
+ A local hash is never called tamper-proof. Server-recorded time is never called trusted time. An IP address is never called identity. Provider receipts are never upgraded into guarantees the provider did not make.
1102
+
1103
+ Run verification continuously:
1104
+
1105
+ ```bash
1106
+ bin/rails clickwrap:verify
1107
+ bin/rails clickwrap:verify EVENT_ID
1108
+ ```
1109
+
1110
+ ## Multi-tenancy, actors, subjects, and authority
1111
+
1112
+ The conventional actor is `User`, but nothing is hard-coded:
1113
+
1114
+ ```ruby
1115
+ Clickwrap.configure do |config|
1116
+ config.actor_class_name = "Account"
1117
+ config.current_actor_method_name = :current_account
1118
+
1119
+ config.find_current_tenant_with = lambda do |controller|
1120
+ controller.current_organization
1121
+ end
1122
+ end
1123
+ ```
1124
+
1125
+ Actors, subjects, and tenants are separate:
1126
+
1127
+ ```ruby
1128
+ Clickwrap.capture!(
1129
+ :logo_rights_declaration,
1130
+ actor: current_user,
1131
+ subject: @organization,
1132
+ tenant: current_organization,
1133
+ http_request: request,
1134
+ submission: clickwrap_submission
1135
+ )
1136
+ ```
1137
+
1138
+ Actor snapshots include only configured fields. Clickwrap never serializes a whole user or domain object into evidence.
1139
+
1140
+ Authentication, actor, organization, and subject are not collapsed into one polymorphic ID. A signed-in employee acting for an organization can be represented explicitly:
1141
+
1142
+ ```ruby
1143
+ Clickwrap.capture!(
1144
+ :organization_terms,
1145
+ actor: current_user,
1146
+ acting_for: current_organization,
1147
+ subject: contract,
1148
+ authentication_context: clickwrap_authentication_context,
1149
+ http_request: request,
1150
+ submission: clickwrap_submission
1151
+ )
1152
+ ```
1153
+
1154
+ By default, the configured actor must match the authenticated principal. Delegation, guardianship, service-account action, and impersonation are rejected unless the policy and host authority adapter explicitly permit them. When permitted, the receipt preserves the authenticated principal, asserted actor, represented party, authority source, role, and verification time as separate facts; Clickwrap does not decide whether that authority is legally sufficient.
1155
+
1156
+ ### Anonymous actors
1157
+
1158
+ Use a host-owned stable opaque identifier—not an IP address:
1159
+
1160
+ ```ruby
1161
+ actor = Clickwrap.anonymous_actor("checkout_#{signed_checkout_id}")
1162
+ ```
1163
+
1164
+ The host owns later account linking and identity/capacity decisions.
1165
+
1166
+ ### System-created records and explicit exemptions
1167
+
1168
+ Seeds, imports, administrators, invitations, and service accounts must never “accept” by omitting a browser parameter or by fabricating a human click:
1169
+
1170
+ ```ruby
1171
+ Clickwrap.exempt!(
1172
+ :signup,
1173
+ actor: Clickwrap.system_actor("database_seed"),
1174
+ subject: user,
1175
+ because: "Generated demo account; no human signup occurred"
1176
+ )
1177
+ ```
1178
+
1179
+ The event is an `exemption`, not an agreement. Policies can permit or reject it explicitly. Every exemption records who/what created it and why.
1180
+
1181
+ Exemptions never satisfy `agreed_to?`, `consented_to?`, or another human-action predicate unless a policy asks the separate `exempted_from?` question. There is no “missing checkbox means system account” inference.
1182
+
1183
+ ## Hotwire, Hotwire Native, APIs, and no-JavaScript flows
1184
+
1185
+ The default helper is server-rendered HTML and works with:
1186
+
1187
+ - normal full-page requests;
1188
+ - Turbo Drive and Turbo Frames;
1189
+ - validation re-renders with no JavaScript;
1190
+ - Hotwire Native web screens;
1191
+ - custom native/API presentations; and
1192
+ - operator/admin surfaces.
1193
+
1194
+ No Stimulus controller is required for correctness. An optional tiny controller may improve disabled-submit affordances, but server validation and evidence capture work without it.
1195
+
1196
+ ### Hotwire Native
1197
+
1198
+ Use the web component whenever possible. Legal-document links can open in the appropriate modal/sheet/external-browser context chosen by the host native shell. The same presentation token and receipt contract applies.
1199
+
1200
+ Native path configuration remains host-owned. Mount/capture routes include both GET and form-action paths so validation stays in the intended navigation context.
1201
+
1202
+ ### JSON/API clients
1203
+
1204
+ Present a policy through the same server-owned presenter:
1205
+
1206
+ ```ruby
1207
+ presentation = Clickwrap.present(
1208
+ :signup,
1209
+ actor: api_actor,
1210
+ locale: :es,
1211
+ capture_channel: :native_api,
1212
+ submit_button_text: "Crear cuenta"
1213
+ )
1214
+
1215
+ render json: presentation
1216
+ ```
1217
+
1218
+ The client renders the declared statements and returns only the signed token plus answers:
1219
+
1220
+ ```ruby
1221
+ Clickwrap.capture!(
1222
+ :signup,
1223
+ actor: api_actor,
1224
+ capture_channel: :native_api,
1225
+ submission: Clickwrap.submission_from(params),
1226
+ client_reported_context: permitted_client_context
1227
+ )
1228
+ ```
1229
+
1230
+ `submission_from` reads only the signed presentation token and the answer keys/types declared by that manifest; unknown keys and malformed choices are rejected. Client-reported values remain explicitly labeled. They can never masquerade as server-observed IP address, server time, trusted identity, or provider-estimated IP geolocation.
1231
+
1232
+ ## Accessible defaults without a fake certification
1233
+
1234
+ The reference helper and views ship with tested:
1235
+
1236
+ - explicit labels and programmatic names;
1237
+ - initially unselected controls;
1238
+ - visible keyboard focus;
1239
+ - high-contrast conventional links;
1240
+ - `aria-invalid` and `aria-describedby` error relationships;
1241
+ - error summary and focus behavior;
1242
+ - keyboard operation;
1243
+ - non-color-only meaning;
1244
+ - no-JavaScript validation;
1245
+ - locale-aware document selection; and
1246
+ - review/correction support for consequential submissions.
1247
+
1248
+ The whole host page still determines placement, clutter, contrast, action wording, accessibility, and notice quality. Clickwrap can lint known hazards; it cannot certify a host application as accessible or an agreement as enforceable.
1249
+
1250
+ ## Operations you can understand at 03:00
1251
+
1252
+ ```bash
1253
+ bin/rails clickwrap:doctor
1254
+ bin/rails clickwrap:publish:plan
1255
+ bin/rails clickwrap:publish
1256
+ bin/rails clickwrap:reacceptance:plan POLICY=current_terms
1257
+ bin/rails clickwrap:verify
1258
+ bin/rails clickwrap:export EVENT_ID
1259
+ bin/rails clickwrap:retention:plan
1260
+ bin/rails clickwrap:retention:apply PLAN=PLAN_ID
1261
+ bin/rails clickwrap:holds:review
1262
+ bin/rails clickwrap:privacy:inventory
1263
+ bin/rails clickwrap:reconcile_external_actions
1264
+ ```
1265
+
1266
+ `clickwrap:doctor` reports objective configuration and data facts:
1267
+
1268
+ ```text
1269
+ ✓ 6 policies compiled
1270
+ ✓ all referenced documents are published and digest-verified
1271
+ ✓ signup has an atomic Devise integration
1272
+ ✓ every required gate has a remediation route
1273
+ ✓ request-derived personal data is off by default
1274
+ ! withdrawal_authorization records IP geolocation city without a review date
1275
+ ! Cloudflare source trust is unverified
1276
+ ✓ no overdue disposition jobs
1277
+ ✓ all checked event digests verify
1278
+ ```
1279
+
1280
+ It never prints “compliant,” “court-proof,” or “audit guaranteed.”
1281
+
1282
+ Metrics and notifications use stable policy/kind/outcome names without raw personal data labels. Sensitive values never appear in ordinary logs, exceptions, `inspect`, notifications, or metrics.
1283
+
1284
+ ## Testing is a first-class API
1285
+
1286
+ Include the helpers in Minitest:
1287
+
1288
+ ```ruby
1289
+ class ActiveSupport::TestCase
1290
+ include Clickwrap::TestHelpers
1291
+ end
1292
+ ```
1293
+
1294
+ Create real, internally consistent test evidence without knowing table details:
1295
+
1296
+ ```ruby
1297
+ receipt = capture_clickwrap(
1298
+ :signup,
1299
+ actor: user,
1300
+ answers: { terms: true, privacy_notice: true }
1301
+ )
1302
+
1303
+ assert_clickwrap_current :signup, actor: user
1304
+ assert_clickwrap_agreed_to :terms, actor: user
1305
+ assert_clickwrap_acknowledged :privacy_notice, actor: user
1306
+ assert_clickwrap_receipt_verifies receipt
1307
+ ```
1308
+
1309
+ System-test helpers drive the actual UI:
1310
+
1311
+ ```ruby
1312
+ complete_clickwrap :signup
1313
+ click_button "Create account"
1314
+ ```
1315
+
1316
+ Fault injection proves required atomicity:
1317
+
1318
+ ```ruby
1319
+ Clickwrap::Testing.fail_next_event_write do
1320
+ assert_raises(Clickwrap::EventWriteFailed) do
1321
+ perform_signup
1322
+ end
1323
+ end
1324
+
1325
+ assert_not User.exists?(email: "person@example.com")
1326
+ assert_no_clickwrap_event :signup
1327
+ ```
1328
+
1329
+ Concurrency, duplicate-submit, stale-token, actor/subject swap, disposition, legal-hold, export round-trip, and legacy-import helpers ship with the gem. No tests make real provider network calls.
1330
+
1331
+ ## The generated initializer explains itself
1332
+
1333
+ The complete initializer is annotated in plain English. A representative configuration looks like:
1334
+
1335
+ ```ruby
1336
+ # config/initializers/clickwrap.rb
1337
+ Clickwrap.configure do |config|
1338
+ config.actor_class_name = "User"
1339
+ config.current_actor_method_name = :current_user
1340
+ config.parent_controller_class_name = "ApplicationController"
1341
+
1342
+ config.find_current_tenant_with = lambda do |controller|
1343
+ controller.current_organization if controller.respond_to?(:current_organization)
1344
+ end
1345
+
1346
+ config.authorize_receipt_access_with = lambda do |controller, receipt|
1347
+ controller.current_user == receipt.actor
1348
+ end
1349
+
1350
+ config.authorize_unredacted_request_evidence_access_with =
1351
+ lambda do |controller, receipt, because|
1352
+ controller.current_user&.security_operator? && because.present?
1353
+ end
1354
+
1355
+ config.identify_actor_with = ->(actor) { actor.to_gid.to_s }
1356
+ # Add only reviewed fields your receipts truly need; never serialize the model.
1357
+ config.snapshot_actor_with = ->(_actor) { {} }
1358
+ config.describe_authentication_with = lambda do |controller|
1359
+ { method: :authenticated_session, authenticated_at: controller.session[:authenticated_at] }
1360
+ end
1361
+
1362
+ config.store_document_contents_in = :database
1363
+ config.digest_canonical_receipts_with = :sha256
1364
+ config.chain_event_history_with = nil
1365
+ config.anchor_event_history_with = nil
1366
+ config.timestamp_receipts_with = nil
1367
+ config.application_version = -> { ENV["RELEASE_SHA"] }
1368
+
1369
+ # Safe defaults: no raw network/browser/geolocation data is stored.
1370
+ config.record_ip_address_by_default = false
1371
+ config.record_browser_user_agent_by_default = false
1372
+ config.record_ip_geolocation_country_by_default = false
1373
+ config.record_ip_geolocation_region_by_default = false
1374
+ config.record_ip_geolocation_city_by_default = false
1375
+ config.record_ip_geolocation_postal_code_by_default = false
1376
+ config.record_ip_geolocation_latitude_and_longitude_by_default = false
1377
+ config.record_ip_geolocation_timezone_by_default = false
1378
+ config.record_ip_geolocation_continent_by_default = false
1379
+ config.record_ip_geolocation_metro_code_by_default = false
1380
+ config.record_ip_geolocation_accuracy_radius_in_kilometers_by_default = false
1381
+
1382
+ # If a default above becomes true, fill in the matching plain-English
1383
+ # reason and a retention rule below. The policy compiler rejects an
1384
+ # enabled default whose purpose or retention is blank.
1385
+ config.reason_for_recording_ip_addresses_by_default = nil
1386
+ config.reason_for_recording_browser_user_agents_by_default = nil
1387
+ config.reason_for_recording_ip_geolocation_by_default = nil
1388
+ config.legal_basis_reference_for_recording_ip_addresses_by_default = nil
1389
+ config.legal_basis_reference_for_recording_browser_user_agents_by_default = nil
1390
+ config.legal_basis_reference_for_recording_ip_geolocation_by_default = nil
1391
+ config.review_default_request_evidence_configuration_on = nil
1392
+
1393
+ config.encrypt_recorded_ip_addresses = true
1394
+ config.encrypt_recorded_browser_user_agents = true
1395
+ config.encrypt_recorded_ip_geolocation = true
1396
+
1397
+ # Nil means every policy that enables the field must supply its own rule.
1398
+ config.delete_recorded_ip_addresses_after = nil
1399
+ config.delete_recorded_browser_user_agents_after = nil
1400
+ config.delete_recorded_ip_geolocation_after = nil
1401
+
1402
+ config.read_ip_address_from_http_request_with =
1403
+ ->(http_request) { http_request.remote_ip }
1404
+
1405
+ config.read_browser_user_agent_from_http_request_with =
1406
+ ->(http_request) { http_request.user_agent }
1407
+
1408
+ config.ip_geolocation_resolver = nil
1409
+ config.fail_capture_when_ip_geolocation_is_unavailable = false
1410
+
1411
+ # Runs only after required evidence and domain state have committed.
1412
+ # Hook failures are reported but can never undo the committed action.
1413
+ config.after_event_is_committed = ->(event) { }
1414
+ config.report_after_commit_failure_with = ->(error, event) { Rails.error.report(error) }
1415
+ end
1416
+ ```
1417
+
1418
+ Every public setting validates its value and reads like a sentence. Class names are resolved lazily for Rails autoloading. Security-critical ambiguity fails at boot instead of becoming a surprising runtime default. A policy-level request-evidence declaration overrides these application defaults, so a high-risk authorization can collect more context without making ordinary signup inherit it.
1419
+
1420
+ ## Generators
1421
+
1422
+ ```bash
1423
+ bin/rails generate clickwrap:install
1424
+ bin/rails generate clickwrap:policy driver_declaration
1425
+ bin/rails generate clickwrap:document terms
1426
+ bin/rails generate clickwrap:views
1427
+ bin/rails generate clickwrap:hardening --database
1428
+ bin/rails generate clickwrap:upgrade
1429
+ ```
1430
+
1431
+ The installer:
1432
+
1433
+ - detects integer/UUID keys and supported database features;
1434
+ - detects Rails authentication and Devise without making either a hard dependency;
1435
+ - stops and explains itself when actor/tenant mappings are ambiguous;
1436
+ - asks before wiring signup or mounting routes;
1437
+ - asks separately about every request-evidence field;
1438
+ - writes plain-English purposes and retention placeholders that must be reviewed;
1439
+ - never overwrites host files without normal Rails generator conflict handling; and
1440
+ - prints a post-install checklist for documents, semantics, privacy, retention, trusted proxies, full-page UI review, and tests.
1441
+
1442
+ Upgrade generators create new migrations. Released migrations are never silently edited underneath an application.
1443
+
1444
+ ## Migrate without inventing history
1445
+
1446
+ ### From FinePrint
1447
+
1448
+ Preview first:
1449
+
1450
+ ```bash
1451
+ bin/rails clickwrap:import:fine_print:plan
1452
+ ```
1453
+
1454
+ Then import:
1455
+
1456
+ ```bash
1457
+ bin/rails clickwrap:import:fine_print
1458
+ ```
1459
+
1460
+ FinePrint contract versions and signatures become explicit `imported_legacy` events. Fields FinePrint did not record—presentation manifest, IP address, CTA, protected action—remain `unknown` or `not_collected`; Clickwrap never synthesizes them.
1461
+
1462
+ ### From `accepted_terms_at`
1463
+
1464
+ ```ruby
1465
+ Clickwrap.import_legacy!(
1466
+ :terms,
1467
+ actor: user,
1468
+ occurred_at: user.accepted_terms_at,
1469
+ known: {
1470
+ document_version: user.terms_version
1471
+ },
1472
+ unknown: %i[
1473
+ exact_document_bytes
1474
+ presentation
1475
+ assertion
1476
+ submit_button_text
1477
+ request_evidence
1478
+ ],
1479
+ because: "Imported from users.accepted_terms_at"
1480
+ )
1481
+ ```
1482
+
1483
+ Imports are append-only, provenance-labeled, idempotent, dry-runnable, and report every unknown. Historical weakness remains visible instead of being laundered into modern certainty.
1484
+
1485
+ ## Extension seams, not dependency soup
1486
+
1487
+ The core has small adapter contracts for:
1488
+
1489
+ - document storage;
1490
+ - actor/tenant resolution;
1491
+ - identity/authentication snapshots;
1492
+ - IP geolocation;
1493
+ - independent checkpoints/anchors;
1494
+ - RFC 3161 or trust-service timestamps;
1495
+ - external clickwrap/signature providers;
1496
+ - object-lock/WORM storage;
1497
+ - PDF rendering;
1498
+ - authorization;
1499
+ - error reporting;
1500
+ - notifications; and
1501
+ - post-commit analytics/auditing.
1502
+
1503
+ Every optional adapter has a no-op default and explicit capability reporting. Installing Clickwrap never pulls in Redis, Sidekiq, Devise, Trackdown, Active Storage, a PDF library, a cloud SDK, or an external service unless the application chooses that integration.
1504
+
1505
+ ActiveSupport notifications are available for instrumentation:
1506
+
1507
+ ```ruby
1508
+ ActiveSupport::Notifications.subscribe("event_committed.clickwrap") do |event|
1509
+ # event payload contains stable IDs and categories, not raw request evidence
1510
+ end
1511
+ ```
1512
+
1513
+ Required writes are never delegated to notifications. Hooks are for observers, not authorization.
1514
+
1515
+ ## What Clickwrap does, what your application owns, and what the receipt proves
1516
+
1517
+ | Area | Clickwrap provides | Your application/counsel owns | Receipt/evidence |
1518
+ |---|---|---|---|
1519
+ | Documents | immutable versions, bytes/digests, locales, publication | text, translation, fairness, legal approval, materiality | exact stored version and digest |
1520
+ | Presentation | tested controls/helper, manifest, token, stale/replay checks | whole-page placement/design, final CTA, accessibility review | server-generated manifest and accepted answers |
1521
+ | Actor | configured reference and authentication snapshot | identity proofing, capacity, authority, guardian/organization rules | exactly which configured actor/context was recorded |
1522
+ | Agreements | version/current-state mechanics | enforceability, governing law, substantive terms | agreement event and historical version |
1523
+ | Privacy notice | acknowledgment mechanics | transparency content and lawful basis for processing | notice version and acknowledgment event |
1524
+ | Consent | purposes, grant/withdrawal/renewal lifecycle | whether consent is the correct basis and whether it is freely given | exact grant/withdrawal history |
1525
+ | Declarations | statement snapshot, expiry/correction/supersession | truth, eligibility, domain validation | what was declared, when, for which subject |
1526
+ | Authorizations | scope, fingerprint, freshness, one-time consumption | domain permission and external-provider consequences | exact evidence-to-outcome binding |
1527
+ | Request evidence | explicit capture, provenance, encryption/redaction/disposition | necessity, lawful basis, disclosure, trusted proxy/source, period | selected fields and honest source/state |
1528
+ | Integrity | canonical digests, verification, optional chains/adapters | keys, infrastructure, access controls, backups, operational procedures | verification result and bounded assurance tier |
1529
+ | Retention | executable rules, holds, dry-run disposition | legally appropriate periods and case-specific holds | retention/hold/disposition history |
1530
+
1531
+ Clickwrap is engineering infrastructure, not legal advice or a compliance certificate.
1532
+
1533
+ ## What Clickwrap deliberately does not become
1534
+
1535
+ Clickwrap does not:
1536
+
1537
+ - draft or approve your legal documents;
1538
+ - choose a GDPR lawful basis or special-category condition;
1539
+ - decide whether a document change is material;
1540
+ - guarantee enforceability, admissibility, accessibility, or audit acceptance;
1541
+ - verify identity, age, capacity, guardianship, or organizational authority;
1542
+ - provide KYC, sanctions screening, fraud scoring, or biometrics;
1543
+ - become a cookie CMP, tracker scanner, or script blocker;
1544
+ - become DocuSign, Ironclad, a notary, a qualified trust-service provider, or a contract lifecycle platform;
1545
+ - call a local hash tamper-proof;
1546
+ - call an IP address identity or IP geolocation physical location;
1547
+ - require forced scrolling or claim it proves reading;
1548
+ - require a sprawling admin/document-authoring suite; or
1549
+ - hide collection behind `compliant: true` or `maximum_evidence: true`.
1550
+
1551
+ Adapters let those systems contribute provider receipts without changing what Clickwrap itself claims.
1552
+
1553
+ ## FinePrint and Clickwrap solve different-sized problems
1554
+
1555
+ [FinePrint](https://github.com/openstax/fine_print/blob/3b75fbcbcfb048ecd2f4ee7c4f0b9bd3d10f7603/README.md#L7-L25) is established Rails prior art for versioned contracts, signatures, gates, and views. Clickwrap should never market itself as the first Rails agreement gem.
1556
+
1557
+ FinePrint’s documented core and [signature model at the audited commit](https://github.com/openstax/fine_print/blob/3b75fbcbcfb048ecd2f4ee7c4f0b9bd3d10f7603/app/models/fine_print/signature.rb#L1-L33) answer:
1558
+
1559
+ ```text
1560
+ Did user U sign version N of contract X?
1561
+ ```
1562
+
1563
+ Clickwrap is for applications that also need to answer:
1564
+
1565
+ ```text
1566
+ Which exact content and presentation was offered?
1567
+ Which explicit statements and choices were made?
1568
+ Did the required evidence and protected outcome commit together?
1569
+ What subject or transaction did it cover?
1570
+ Was it withdrawn, corrected, superseded, expired, or consumed?
1571
+ Can the complete receipt be reproduced and verified independently?
1572
+ Can optional personal request evidence be disposed of honestly?
1573
+ ```
1574
+
1575
+ The goal is to be easier in the first five minutes and dramatically stronger after five years in production—not FinePrint with more columns.
1576
+
1577
+ ## Legal and evidentiary posture
1578
+
1579
+ Electronic form does not cure an invalid underlying transaction, missing capacity/authority, or a special formality. The US E-SIGN Act preserves electronic validity while retaining substantive requirements and exclusions ([15 U.S.C. § 7001](https://www.law.cornell.edu/uscode/text/15/7001); [15 U.S.C. § 7003](https://www.law.cornell.edu/uscode/text/15/7003)). Electronic form also does not make an unfair term fair ([Directive 93/13/EEC](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=celex%3A31993L0013)). EU eIDAS distinguishes ordinary electronic evidence from qualified electronic signatures and their specific legal effect ([Regulation (EU) No 910/2014, Article 25](https://eur-lex.europa.eu/eli/reg/2014/910/2024-05-20/eng)).
1580
+
1581
+ US appellate formation decisions evaluate conspicuous notice and unambiguous assent in the context of the whole interface; no checkbox color or placement is a universal safe harbor ([Berman v. Freedom Financial Network](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/05/20-16900.pdf); [Tejon v. Zeus Networks](https://media.ca11.uscourts.gov/opinions/pub/files/202411114.pdf); [Toth v. Everly Well](https://www.ca1.uscourts.gov/sites/ca1/files/opnfiles/23-1727P-01A.pdf)).
1582
+
1583
+ GDPR consent must be demonstrable, distinguishable, and withdrawable, but consent is only one possible lawful basis. A privacy-information acknowledgment is not blanket consent ([GDPR Article 6](https://eur-lex.europa.eu/eli/reg/2016/679/art_6/oj/eng); [GDPR Article 7](https://eur-lex.europa.eu/eli/reg/2016/679/art_7/oj/eng); [AEPD FAQ 02.48](https://www.aepd.es/preguntas-frecuentes/2-tus-obligaciones-como-responsable-del-tratamiento/6-el-deber-de-informacion/FAQ-0248-sobre-si-el-usuario-tiene-que-dar-consentimiento-a-clausula-de-privacidad)). GDPR also requires purpose limitation, data minimization, storage limitation, transparency, and security; “collect everything forever” is not the evidence-maximizing default ([Article 5](https://eur-lex.europa.eu/eli/reg/2016/679/art_5/oj/eng); [Article 13](https://eur-lex.europa.eu/eli/reg/2016/679/art_13/oj/eng); [Article 32](https://eur-lex.europa.eu/eli/reg/2016/679/art_32/oj/eng)).
1584
+
1585
+ These sources motivate Clickwrap’s design. They do not turn the gem into legal advice or a universal safe harbor.
1586
+
1587
+ ## Security model
1588
+
1589
+ Clickwrap treats these as hostile until verified:
1590
+
1591
+ - policy/document/version/validity values submitted by the client;
1592
+ - stale or replayed presentation tokens;
1593
+ - swapped actor, tenant, subject, or transaction IDs;
1594
+ - forwarded IP and Cloudflare headers outside a verified proxy path;
1595
+ - client timestamps and client-reported identity/location;
1596
+ - duplicate/concurrent submits;
1597
+ - mutable document sources;
1598
+ - after-commit analytics and provider callbacks; and
1599
+ - imported evidence without provider provenance.
1600
+
1601
+ Security-sensitive values are server-owned, signed/bound, rechecked inside the transaction, and represented by stable failure results. Rails’ CSRF/session/authentication protections remain host responsibilities. Encryption keys, signing keys, and adapter credentials use Rails credentials or application-provided key providers and support rotation with versioned key identifiers.
1602
+
1603
+ Report vulnerabilities privately according to `SECURITY.md`. Do not open a public issue containing an exploit or real evidence/PII.
1604
+
1605
+ ## Compatibility
1606
+
1607
+ The ideal supported matrix is:
1608
+
1609
+ - Ruby 3.2 through current Ruby, tested explicitly;
1610
+ - Rails 7.1 through current Rails 8.x;
1611
+ - PostgreSQL, SQLite, and MySQL for all documented portable core behavior;
1612
+ - adapter-specific hardening clearly marked and tested;
1613
+ - Rails authentication and Devise, both optional integrations;
1614
+ - Turbo/Hotwire and ordinary HTML;
1615
+ - integer and UUID primary keys;
1616
+ - multi-database applications when evidence and protected action share the documented transaction boundary; and
1617
+ - API-only applications for model/service/JSON receipt APIs, with HTML engine mounting optional.
1618
+
1619
+ The gem depends only on the Rails components its approved surface needs. It does not depend on the `rails` meta-gem, Redis, a job backend, a JavaScript runtime, an external provider, or a CSS framework.
1620
+
1621
+ The actual released gemspec and CI matrix—not this wishlist—are authoritative once implementation exists.
1622
+
1623
+ ## Stability and upgrade promise
1624
+
1625
+ Clickwrap follows semantic versioning for its documented Ruby/Rails APIs, but persisted evidence gets a stricter promise:
1626
+
1627
+ - every released receipt schema, canonicalization profile, digest field, event action, and lifecycle meaning has a permanent golden fixture;
1628
+ - new gem versions continue verifying old receipts even when they stop creating that old schema;
1629
+ - a format change gets a new explicit schema/version and verifier, never a silent reinterpretation;
1630
+ - upgrade generators add migrations and report their exact effects; released migration files are never edited under an installed application;
1631
+ - destructive or lossy data transitions require a plan, explicit operator action, and rollback/export guidance;
1632
+ - deprecations name the replacement and remain executable for a documented window; and
1633
+ - security fixes distinguish a vulnerable capture path from a verifier/display-only issue so operators know what historical evidence, if any, needs review.
1634
+
1635
+ The project publishes the CI matrix, generator diffs, benchmark script, receipt golden fixtures, threat-model changes, and upgrade notes with every release. “It still boots” is not enough for a gem whose value is long-lived evidence.
1636
+
1637
+ ## Performance
1638
+
1639
+ The ordinary capture path is one bounded database transaction with no network call. Documents and compiled policies are cached by immutable digest. Request geolocation, timestamp providers, external anchors, PDFs, and analytics are optional and never hidden in the simple path.
1640
+
1641
+ There is no global event-history mutex. Sequence/chain scope is per tenant or aggregate, benchmarked under contention, and independently checkpointed where enabled. Bulk export streams records and verifies incrementally.
1642
+
1643
+ Performance claims are published only with reproducible benchmarks against supported databases.
1644
+
1645
+ ## FAQ
1646
+
1647
+ ### Is this an electronic-signature gem?
1648
+
1649
+ It captures electronic evidence of explicit actions and can import/provider-bind signature receipts. It does not call ordinary clickwrap a qualified electronic signature, notarization, or trusted identity proof.
1650
+
1651
+ ### Does a user have to open or scroll through the document?
1652
+
1653
+ Not by universal default. Clickwrap makes the document available before action and records the exact presentation. A policy can require an accurately observed open/review interaction when the host has a real requirement, but Clickwrap never equates scrolling with reading or understanding.
1654
+
1655
+ ### Should I record IP addresses and geolocation?
1656
+
1657
+ Only for policies with a present, documented purpose and reviewed access/retention posture. They can corroborate request context but do not repair weak notice or prove identity/physical location. All such fields default off.
1658
+
1659
+ ### Can I use Clickwrap without Devise?
1660
+
1661
+ Yes. Devise and Rails authentication are convenience adapters over the same public capture APIs.
1662
+
1663
+ ### Can one policy contain several documents and statements?
1664
+
1665
+ Yes. The receipt preserves each document/version, statement, choice, and ordering independently. Agreement, acknowledgment, and optional consent controls remain semantically separate even when one page presents them together.
1666
+
1667
+ ### Can I keep my domain-specific declaration or authorization model?
1668
+
1669
+ Yes—and usually should. Clickwrap complements domain models; it does not replace your payout, certification, identity, employment, or eligibility rules.
1670
+
1671
+ ### Can Clickwrap prove the user saw the page?
1672
+
1673
+ It can prove the server generated and accepted a bound presentation manifest and record accurately observed interactions. It cannot prove human attention, comprehension, exact pixels, or legal sufficiency from a database row.
1674
+
1675
+ ### What happens if Clickwrap is temporarily unavailable?
1676
+
1677
+ Required evidence fails closed: the same-database protected action rolls back. Optional after-commit hooks fail independently and are reported. Applications can define deliberate emergency/system exemptions with explicit actor and reason; there is no silent rescue-and-continue path.
1678
+
1679
+ ### Can I delete evidence?
1680
+
1681
+ Yes, according to explicit retention/disposition policy and legal holds. Optional request evidence is separately disposable. Core historical evidence is never silently deleted through an actor association, and disposition is itself recorded.
1682
+
1683
+ ### Is this GDPR compliant?
1684
+
1685
+ No gem can answer that universally. Clickwrap provides privacy-aware mechanisms and truthful defaults. The host remains responsible for lawful basis, necessity, transparency, data-subject rights, security, retention, processors/transfers, DPIAs, and jurisdiction-specific requirements.
1686
+
1687
+ ## Development
1688
+
1689
+ ```bash
1690
+ bin/setup
1691
+ bin/test
1692
+ bin/rubocop
1693
+ bin/rails test
1694
+ ```
1695
+
1696
+ The project uses Minitest, a dummy Rails application, SimpleCov, RuboCop, Appraisal matrices, SQLite/PostgreSQL/MySQL integration lanes, concurrency/fault tests, generator tests, Brakeman where relevant, and independent receipt-verifier golden fixtures.
1697
+
1698
+ Every change to canonicalization, schema, receipts, migrations, cryptographic fields, or lifecycle behavior must prove backward verification against all released fixtures.
1699
+
1700
+ ## Contributing
1701
+
1702
+ Bug reports and focused pull requests are welcome once the repository opens for implementation. Changes to public vocabulary or evidence claims require corresponding documentation, source review, migration/compatibility analysis, and proof-integration coverage.
1703
+
1704
+ Please do not use issues to request jurisdiction-specific legal advice or ask maintainers to approve legal text.
1705
+
1706
+ ## License
1707
+
1708
+ MIT.