clickwrap 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +90 -0
  3. data/CHANGELOG.md +612 -0
  4. data/README.md +830 -1204
  5. data/SECURITY.md +33 -0
  6. data/app/assets/stylesheets/clickwrap.css +241 -0
  7. data/app/controllers/clickwrap/application_controller.rb +79 -0
  8. data/app/controllers/clickwrap/captures_controller.rb +145 -0
  9. data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
  10. data/app/controllers/clickwrap/receipts_controller.rb +115 -0
  11. data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
  12. data/app/helpers/clickwrap/engine_helper.rb +97 -0
  13. data/app/views/clickwrap/captures/show.html.erb +34 -0
  14. data/app/views/clickwrap/receipts/index.html.erb +38 -0
  15. data/app/views/clickwrap/receipts/show.html.erb +91 -0
  16. data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
  17. data/app/views/clickwrap/shared/_fields.html.erb +100 -0
  18. data/app/views/clickwrap/shared/_statement.html.erb +105 -0
  19. data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
  20. data/config/locales/en.yml +160 -0
  21. data/config/locales/es.yml +138 -0
  22. data/config/routes.rb +41 -0
  23. data/exe/clickwrap +374 -0
  24. data/guides/README.md +30 -0
  25. data/guides/accessibility.md +249 -0
  26. data/guides/consent-and-lifecycle.md +299 -0
  27. data/guides/integrating.md +610 -0
  28. data/guides/integrity.md +212 -0
  29. data/guides/migrating.md +335 -0
  30. data/guides/naming.md +320 -0
  31. data/guides/organizations.md +320 -0
  32. data/guides/receipts-and-verification.md +415 -0
  33. data/guides/request-evidence.md +512 -0
  34. data/guides/retention-and-legal-holds.md +438 -0
  35. data/lib/clickwrap/actor_proxy.rb +147 -0
  36. data/lib/clickwrap/anonymous_actor.rb +47 -0
  37. data/lib/clickwrap/authority.rb +174 -0
  38. data/lib/clickwrap/canonical_json.rb +216 -0
  39. data/lib/clickwrap/capture/event_builder.rb +220 -0
  40. data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
  41. data/lib/clickwrap/capture.rb +650 -0
  42. data/lib/clickwrap/configuration.rb +1129 -0
  43. data/lib/clickwrap/controller_helpers.rb +758 -0
  44. data/lib/clickwrap/current_state.rb +282 -0
  45. data/lib/clickwrap/digest.rb +125 -0
  46. data/lib/clickwrap/doctor.rb +418 -0
  47. data/lib/clickwrap/document_definition.rb +255 -0
  48. data/lib/clickwrap/document_renderer.rb +83 -0
  49. data/lib/clickwrap/document_renderers/markdown.rb +175 -0
  50. data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
  51. data/lib/clickwrap/dsl/policy_builder.rb +462 -0
  52. data/lib/clickwrap/dsl/retention_builder.rb +89 -0
  53. data/lib/clickwrap/durable_commit_callback.rb +37 -0
  54. data/lib/clickwrap/engine.rb +184 -0
  55. data/lib/clickwrap/errors.rb +181 -0
  56. data/lib/clickwrap/form_builder_extensions.rb +341 -0
  57. data/lib/clickwrap/front_matter.rb +67 -0
  58. data/lib/clickwrap/identifier.rb +112 -0
  59. data/lib/clickwrap/import/external_receipt.rb +241 -0
  60. data/lib/clickwrap/import/fine_print.rb +290 -0
  61. data/lib/clickwrap/import/legacy.rb +450 -0
  62. data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
  63. data/lib/clickwrap/integrity/anchor.rb +130 -0
  64. data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
  65. data/lib/clickwrap/integrity/attestor.rb +221 -0
  66. data/lib/clickwrap/integrity/chain.rb +313 -0
  67. data/lib/clickwrap/integrity/timestamp.rb +143 -0
  68. data/lib/clickwrap/ip_geolocation/location.rb +112 -0
  69. data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
  70. data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
  71. data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
  72. data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
  73. data/lib/clickwrap/ip_geolocation.rb +16 -0
  74. data/lib/clickwrap/lifecycle.rb +534 -0
  75. data/lib/clickwrap/linter.rb +382 -0
  76. data/lib/clickwrap/localized_text.rb +101 -0
  77. data/lib/clickwrap/macros.rb +203 -0
  78. data/lib/clickwrap/models/application_record.rb +20 -0
  79. data/lib/clickwrap/models/chain_head.rb +79 -0
  80. data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
  81. data/lib/clickwrap/models/disposition_plan.rb +208 -0
  82. data/lib/clickwrap/models/document.rb +46 -0
  83. data/lib/clickwrap/models/document_version.rb +163 -0
  84. data/lib/clickwrap/models/event.rb +743 -0
  85. data/lib/clickwrap/models/event_document.rb +79 -0
  86. data/lib/clickwrap/models/event_statement.rb +92 -0
  87. data/lib/clickwrap/models/external_action.rb +150 -0
  88. data/lib/clickwrap/models/integrity_attestation.rb +90 -0
  89. data/lib/clickwrap/models/legal_hold.rb +81 -0
  90. data/lib/clickwrap/models/policy_revision.rb +115 -0
  91. data/lib/clickwrap/models/presentation.rb +59 -0
  92. data/lib/clickwrap/models/receipt_access.rb +53 -0
  93. data/lib/clickwrap/models/recording_sequence.rb +21 -0
  94. data/lib/clickwrap/models/request_evidence.rb +378 -0
  95. data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
  96. data/lib/clickwrap/models/statement_state.rb +130 -0
  97. data/lib/clickwrap/pending_receipt.rb +177 -0
  98. data/lib/clickwrap/policy.rb +283 -0
  99. data/lib/clickwrap/presentation_manifest.rb +210 -0
  100. data/lib/clickwrap/presenter.rb +716 -0
  101. data/lib/clickwrap/privacy.rb +419 -0
  102. data/lib/clickwrap/protected_outcome.rb +120 -0
  103. data/lib/clickwrap/receipt.rb +606 -0
  104. data/lib/clickwrap/receipt_html.rb +235 -0
  105. data/lib/clickwrap/receipt_verifier.rb +978 -0
  106. data/lib/clickwrap/reference.rb +44 -0
  107. data/lib/clickwrap/registration.rb +236 -0
  108. data/lib/clickwrap/registry.rb +54 -0
  109. data/lib/clickwrap/remediation_token.rb +155 -0
  110. data/lib/clickwrap/request_evidence_extractor.rb +590 -0
  111. data/lib/clickwrap/request_evidence_policy.rb +261 -0
  112. data/lib/clickwrap/retention/applier.rb +231 -0
  113. data/lib/clickwrap/retention/disposition.rb +221 -0
  114. data/lib/clickwrap/retention/planner.rb +502 -0
  115. data/lib/clickwrap/retention_class.rb +97 -0
  116. data/lib/clickwrap/reviewed_text.rb +28 -0
  117. data/lib/clickwrap/schema_requirements.rb +196 -0
  118. data/lib/clickwrap/services/authorize_external_action.rb +149 -0
  119. data/lib/clickwrap/services/load_policies.rb +69 -0
  120. data/lib/clickwrap/services/publish_documents.rb +251 -0
  121. data/lib/clickwrap/services/validate_policy_references.rb +166 -0
  122. data/lib/clickwrap/statement.rb +248 -0
  123. data/lib/clickwrap/subject_fingerprint.rb +28 -0
  124. data/lib/clickwrap/submission.rb +169 -0
  125. data/lib/clickwrap/system_actor.rb +31 -0
  126. data/lib/clickwrap/test_helpers.rb +676 -0
  127. data/lib/clickwrap/testing.rb +211 -0
  128. data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
  129. data/lib/clickwrap/verification.rb +504 -0
  130. data/lib/clickwrap/version.rb +12 -1
  131. data/lib/clickwrap/view_helpers.rb +190 -0
  132. data/lib/clickwrap/vocabulary.rb +294 -0
  133. data/lib/clickwrap.rb +497 -7
  134. data/lib/generators/clickwrap/document_generator.rb +164 -0
  135. data/lib/generators/clickwrap/hardening_generator.rb +177 -0
  136. data/lib/generators/clickwrap/install_generator.rb +1287 -0
  137. data/lib/generators/clickwrap/link_generator.rb +56 -0
  138. data/lib/generators/clickwrap/policy_generator.rb +118 -0
  139. data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
  140. data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
  141. data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
  142. data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
  143. data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
  144. data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
  145. data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
  146. data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
  147. data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
  148. data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
  149. data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
  150. data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
  151. data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
  152. data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
  153. data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
  154. data/lib/generators/clickwrap/views_generator.rb +101 -0
  155. data/lib/tasks/clickwrap.rake +569 -0
  156. metadata +311 -16
@@ -0,0 +1,415 @@
1
+ # Receipts and verification
2
+
3
+ A receipt answers "show me exactly what the application recorded." There is one canonical JSON body
4
+ per event and one HTML projection of the same facts — never a different set of facts, and never
5
+ a stronger claim than the JSON makes.
6
+
7
+ ```ruby
8
+ receipt = Clickwrap.receipt(event_id)
9
+
10
+ receipt.to_canonical_json # the verifiable bytes
11
+ receipt.to_html # the human projection
12
+ receipt.verify # against this application's data
13
+ ```
14
+
15
+ `to_pdf` deliberately raises. A PDF library is not a dependency of an evidence gem, and a PDF
16
+ is a rendering of the receipt rather than the record. Run your own pipeline over `to_html` if
17
+ you need one.
18
+
19
+ ---
20
+
21
+ ## Anatomy
22
+
23
+ Everything below describes the released receipt schema. Keys whose value would be empty are
24
+ omitted rather than written as `null` — the one exception being request evidence, which always
25
+ reports an explicit state object.
26
+
27
+ ### Top level
28
+
29
+ | Key | What it holds |
30
+ |---|---|
31
+ | `schema` | `"clickwrap.receipt.v1"`. A verifier that does not know this name stops rather than guessing |
32
+ | `event_id` | The stable identifier the domain row can reference |
33
+ | `event_type` | What produced the event: `capture`, `withdrawal`, `correction`, `supersession`, `expiry`, `consumption`, `revocation`, `renewal`, `scope_change`, `exemption`, `imported_legacy`, `external_receipt`, `disposition`, `legal_hold_placed`, `legal_hold_released`, `receipt_access`, or `provider_outcome` |
34
+ | `recorded_at_by_server` | When this application wrote the row down |
35
+ | `occurred_at` | When the act happened, according to whatever recorded it. For a live capture these coincide; for an import they usually do not, and the gap is itself evidence |
36
+ | `verifier_instructions` | A sentence telling a reader how to check the file without this application |
37
+
38
+ ### `policy`
39
+
40
+ | Key | What it holds |
41
+ |---|---|
42
+ | `key` | The policy key as declared in `Clickwrap.policy` |
43
+ | `revision` | The digest of the compiled policy revision frozen at capture. Historical receipts do not need current source code to explain what the policy meant |
44
+ | `retention_class` | Which retention class governed this event |
45
+
46
+ ### `actor`
47
+
48
+ | Key | What it holds |
49
+ |---|---|
50
+ | `type` | The actor model's class name |
51
+ | `reference` | The stable reference produced by `config.identify_actor_with`: a model's `clickwrap_actor_reference`, otherwise GlobalID when available, otherwise `ClassName/id`. The recorded string survives the row being deleted |
52
+ | `attribution.method` | One of `authenticated_session`, `account_registration`, `operator_session`, `api_credential`, `anonymous_identifier`, `system_process`, `imported_provider`, `unknown`. None of these is an identity claim; each says which application-supplied context was recorded |
53
+ | `attribution.authenticated` | True only when the method was `authenticated_session` |
54
+ | `authentication_method` | The host's own description from `config.describe_authentication_with`. Its default claims `{ method: :authenticated_session }` only when the controller's configured current-actor method returns someone, and `{}` otherwise — a signup form and a controller with no authentication at all both record nothing rather than a claim nobody checked |
55
+ | `snapshot` | Only the actor fields the host named in `config.snapshot_actor_with`. Clickwrap never serializes a whole model into evidence |
56
+ | `acting_for` | Present only for delegated action: the represented party's type and reference, the authority source, the authority role, and when authority was verified — as four separate facts. Clickwrap does not decide whether that authority was sufficient |
57
+ | `tenant` | The tenant key, when the capture had one |
58
+ | `subject.reference` / `subject.fingerprint` | Which domain object the act was about, and the host-computed fingerprint that binds it |
59
+
60
+ ### `acts[]`
61
+
62
+ One entry per statement. A policy with several statements produces one event and one receipt,
63
+ but the statements never collapse into one meaning.
64
+
65
+ | Key | What it holds |
66
+ |---|---|
67
+ | `statement` | The statement key |
68
+ | `kind` | `agreement`, `acknowledgment`, `consent`, `declaration`, `attestation`, or `authorization` |
69
+ | `action` | What was recorded for this kind — see [the lifecycle guide](consent-and-lifecycle.md) |
70
+ | `assertion` | The **resolved sentence** bound to the generated presentation, not an I18n key. A key's meaning can change in a later deploy; then the receipt no longer says what the server asked |
71
+ | `locale` | The locale that sentence was resolved in |
72
+ | `required` / `answered` / `answer` | Whether the statement was required, whether it was answered, and what the answer was |
73
+ | `purpose` | The purpose key, for consent |
74
+ | `expires_at` | When this act stops being current, for kinds that can expire |
75
+ | `one_time` | Present only when the act is a one-time authorization |
76
+ | `subject_fingerprint` | The fingerprint recorded for this statement's subject binding |
77
+
78
+ ### `documents[]`
79
+
80
+ | Key | What it holds |
81
+ |---|---|
82
+ | `statement` | Which statement this document was attached to |
83
+ | `key`, `version`, `locale` | Which exact document version was bound |
84
+ | `source_media_type` / `source_digest` | The media type and digest of the original source bytes, as they stood at capture |
85
+ | `rendered_media_type` / `rendered_digest` | The media type and digest of the rendered representation actually offered |
86
+ | `renderer` | The named renderer and sanitizer versions that produced the rendered bytes, when applicable |
87
+ | `ordinal` | The document's order within the presentation |
88
+
89
+ Two digests, because "this Markdown file existed" and "this rendered representation was
90
+ offered" are different claims. The digests are copied onto the event rather than only
91
+ referenced, so verification can detect a document-version row edited in place instead of
92
+ trusting the column it is supposed to be checking.
93
+
94
+ ### `presentation`
95
+
96
+ | Key | What it holds |
97
+ |---|---|
98
+ | `manifest_digest` | The canonical digest of the presentation manifest the server generated |
99
+ | `submit_button_text` | The exact call-to-action wording in that manifest |
100
+ | `combined_sentence` | Present only when the offer was ONE control carrying one composed sentence ("I agree to the Terms of Service and I acknowledge the Privacy Policy."). The acts say what was recorded; this says what the person read, which is what a substitution argument is actually about |
101
+ | `combined_statements` | Which statement keys that one answer covered |
102
+ | `locale` | The locale the presentation was rendered in |
103
+ | `capture_channel` | `web_browser`, `native_app`, `api_client`, `operator`, `background_job`, `imported_provider`, or `system` |
104
+ | `offered_at` | When the manifest was issued |
105
+ | `proves` | A sentence, inside the receipt itself, saying what this establishes: that the server generated the manifest and accepted a submission bound to it — and not that the person read or understood the documents, saw particular pixels, or received a sufficient interface |
106
+
107
+ ### `outcome`, `lifecycle`, `provider`
108
+
109
+ `outcome` is the protected action's in-transaction result snapshot when the policy configured
110
+ `record_protected_outcome_with`. The callback receives the block's return value and should use
111
+ `Clickwrap.protected_outcome(action:, record:, facts:, state: nil)`. The resulting reference,
112
+ non-empty canonical facts, and fingerprint over the complete claim are validated before commit;
113
+ a malformed or stale fingerprint rolls the domain action and evidence back together. Without a
114
+ recorder, the receipt says only that the named policy, bound subject, evidence event, and block
115
+ committed together — Clickwrap does not guess what a host method meant. A pending external outbox
116
+ does not claim a completed local outcome; its provider result is appended later.
117
+
118
+ `lifecycle` carries `root_event_id`, `predecessor_event_id`, and every successor event with its
119
+ type, time, reason, canonical event body, and event digest. This is where a withdrawal,
120
+ correction, supersession, expiry, consumption, or disposition shows up. Ordinary lifecycle
121
+ changes append instead of overwriting the earlier event; reviewed core disposition is the named
122
+ exception that removes a fixed payload while retaining a linked tombstone.
123
+
124
+ That makes an exported receipt a projection at the moment it was exported. A later export may
125
+ add lifecycle successors or integrity attestations, so its top-level `receipt_digest` may be
126
+ different even though both exports are valid. The embedded canonical `event` object and its
127
+ `integrity.event_digest` are the stable historical anchor for the original event: lifecycle
128
+ history grows by appending linked events rather than changing that body. Keep an exported file
129
+ if the exact export itself matters; do not use byte equality between exports as a current-state
130
+ test.
131
+
132
+ `provider` appears only on imported external receipts, and carries the provider name, its event
133
+ ID, its verification status, and a `note` stating plainly that Clickwrap did not present this
134
+ content or observe this action.
135
+
136
+ ### `request_evidence`
137
+
138
+ Three keys — `ip_address`, `browser_user_agent`, `ip_geolocation` — each always present, each
139
+ carrying a `state`. See [the request-evidence guide](request-evidence.md) for the field-level
140
+ dictionary. What matters here is that raw values are **not** in the canonical body by default.
141
+ They live in a separately encrypted annex with its own authorization, retention, hold, and
142
+ disposition state, and the body carries only a keyed digest binding the two.
143
+
144
+ That boundary is the whole reason the annex exists. Welding an IP address into the core event
145
+ would force its schedule to match the agreement payload. Here each annex category can go on its
146
+ own clock while the retained core event stays intact and verifiable.
147
+
148
+ An export that reveals request evidence is a different document from a redacted one, so it
149
+ carries a different `receipt_digest`. That is correct: each file verifies as the file it
150
+ actually is.
151
+
152
+ ### `retention`
153
+
154
+ `class`, `core_event_retained_until`, `retention_rule`, `core_event_disposed_at`, and
155
+ `on_legal_hold`. See [the retention guide](retention-and-legal-holds.md).
156
+
157
+ ### `system`
158
+
159
+ `gem_version`, `application_version`, `template_version`, `canonical_schema_version`, and
160
+ `verifier_version` — so a reader can tell which code wrote the receipt and which code read it.
161
+
162
+ ---
163
+
164
+ ## The two digests
165
+
166
+ `integrity` carries both `receipt_digest` and `event_digest`. They are different values, they
167
+ cover different bytes, and confusing them is the single easiest way to overstate what a
168
+ verification result means.
169
+
170
+ | | `integrity.receipt_digest` | `integrity.event_digest` |
171
+ |---|---|---|
172
+ | Covers | This receipt body, with only `integrity.receipt_digest` removed, canonicalized per RFC 8785 | The **event's own** canonical body embedded at top-level `event`: a different object, with different keys, built when the event was written |
173
+ | Computed | When the receipt is produced, by whoever produced it | Once, at capture, and stored on the event row |
174
+ | Checkable from a file alone | **Yes.** | **Yes while the event payload is retained.** After reviewed core disposition, the verifier checks the retained tombstone and exact digest-linked disposition successor instead and labels the original digest check incomplete |
175
+ | Answers | "Has this exported projection changed since this digest was taken?" | "Does the embedded historical event body match the digest recorded when that event was finalized?" In-app verification additionally compares against the database row |
176
+
177
+ The exclusion of `integrity` is not a convenience. A digest cannot cover itself: the moment it
178
+ is written into the object, the object's bytes change and no amount of recomputation converges.
179
+ So the digest is taken over the body before `integrity.receipt_digest` is attached, and
180
+ verification reproduces that by removing exactly that field. Everything else — including the
181
+ rest of `integrity`, `verifier_instructions`, and any host `x_`-prefixed extension — is inside the digest. A key
182
+ excluded without being named would be a key anyone could edit freely.
183
+
184
+ The receipt embeds the event's canonical body under `event`, so the standalone verifier re-derives
185
+ `event_digest` and also checks every duplicated receipt projection against it. If reviewed
186
+ retention has removed the original payload, it cannot truthfully re-derive those deleted bytes;
187
+ it instead requires a minimal tombstone and exactly one digest-checked `disposition` successor
188
+ whose event ID, predecessor/root link, original digest, and disposition time agree. That check is
189
+ reported as incomplete/documented disposition, never as an ordinary verifying event digest.
190
+
191
+ `integrity` also carries `previous_event_digest`, `chain_scope`, and `chain_sequence` when
192
+ chaining is on; per-category request-evidence binding digests, algorithm, key ID, and binding
193
+ status; immutable timestamp/anchor attestation results; and `tier` plus `detects`, which state in
194
+ the receipt itself exactly what the assurance level detects. See [the integrity guide](integrity.md).
195
+
196
+ ---
197
+
198
+ ## Canonicalization
199
+
200
+ Receipts are digested and verified by code that may be years newer than the code that wrote
201
+ them, and by verifiers written in other languages. So the bytes have to be reproducible from
202
+ the data alone: no Ruby object serialization, no YAML, no hash insertion order, no database
203
+ column order, no locale-dependent number formatting.
204
+
205
+ The base is the [JSON Canonicalization Scheme, RFC 8785](https://www.rfc-editor.org/rfc/rfc8785)
206
+ *(technical standard)*. Two of its rules bite in practice, and Clickwrap implements both
207
+ explicitly rather than relying on Ruby's defaults:
208
+
209
+ - **Object keys sort by UTF-16 code units**, not UTF-8 bytes. Ruby compares strings by UTF-8,
210
+ which orders characters outside the Basic Multilingual Plane differently, so keys are encoded
211
+ to UTF-16BE before comparison.
212
+ - **Numbers follow the ECMAScript `Number::toString` algorithm.** Ruby writes `1.0` where
213
+ ECMAScript writes `1`, and `1.0e-05` where ECMAScript writes `0.00001`. Clickwrap rebuilds
214
+ the ECMAScript form from Ruby's shortest round-trip digits.
215
+
216
+ Integers whose magnitude exceeds 2^53 − 1 raise rather than serialize, because they cannot
217
+ survive the IEEE 754 double round trip RFC 8785 assumes and another verifier could not
218
+ reproduce the bytes. Record large identifiers as strings.
219
+
220
+ ### The Clickwrap profile
221
+
222
+ Five additions on top of RFC 8785. They exist so two verifiers never disagree about what a
223
+ value meant:
224
+
225
+ | Rule | Why |
226
+ |---|---|
227
+ | **Timestamps are UTC strings with exactly six fractional digits and a `Z` suffix** (`"2026-08-15T12:34:56.123456Z"`) | Fixed width, so nobody has to decide whether a trailing zero was significant. Local offsets and variable precision are two verifiers' worth of disagreement |
228
+ | **Digests are `"<algorithm>:<lowercase hex>"`** (`"sha256:9f86d0…"`) | An auditor never has to guess which function produced a bare hex string, and a future release can add an algorithm without making old events unreadable. Keyed digests use `"hmac-sha256:…"` |
229
+ | **Identifiers are strings, never numbers** | A numeric ID is a lossy round trip through a double and a formatting decision waiting to happen |
230
+ | **A value that was never collected is an explicit state object**, never `null` and never a missing key | `null` and "absent" both mean four different things — not configured, unavailable, redacted, deleted. The state object says which |
231
+ | **Host extensions use keys prefixed `x_`** | So a verifier can tell your additions from the schema's, and so your additions are still inside the digest |
232
+
233
+ ---
234
+
235
+ ## Verifying inside the application
236
+
237
+ ```ruby
238
+ result = receipt.verify
239
+ result.success? # => true
240
+ result.error # => :declaration_expired, or nil
241
+ result.message # localized human explanation
242
+ result.details # stable machine-readable facts, no surprise personal data
243
+ ```
244
+
245
+ This path has the database, so it checks what a file cannot: that the event's own digest still
246
+ matches the row, that document-version rows still carry the digests the event recorded, that
247
+ the lifecycle state is what the policy requires, and that chain links line up with their
248
+ neighbours.
249
+
250
+ Across the whole store, or one event:
251
+
252
+ ```bash
253
+ bin/rails clickwrap:verify
254
+ bin/rails clickwrap:verify EVENT_ID
255
+ ```
256
+
257
+ Chain walks are their own operation and report their own vocabulary:
258
+
259
+ ```ruby
260
+ result = Clickwrap::Integrity::Chain.verify
261
+ result.success? # => true
262
+ result.counts # => {"checked" => …, "verified" => …, "breaks" => 0, "scopes" => …}
263
+ result.first_break # => nil, or a Break with a stable `reason`
264
+ ```
265
+
266
+ The break reasons are stable symbols so a monitor can branch on them without matching English:
267
+ `digest_does_not_match`, `previous_digest_does_not_link`, `sequence_gap`,
268
+ `earlier_events_missing`.
269
+
270
+ ---
271
+
272
+ ## Verifying outside the application
273
+
274
+ ```ruby
275
+ Clickwrap::Receipt.verify(canonical_json, documents: document_files)
276
+ ```
277
+
278
+ ```bash
279
+ clickwrap verify receipt.json --documents ./receipt-documents
280
+ clickwrap verify receipt.json --documents ./receipt-documents --json
281
+ ```
282
+
283
+ The CLI's dependency list is the feature. It requires JSON, the canonicalizer, the digest
284
+ helpers, the error classes, and the version constants — no Rails, no Active Record, no
285
+ database, no engine, no host application, no policy source, no network. A verifier that had to
286
+ boot the application which produced the evidence could only ever tell you that the application
287
+ agrees with itself.
288
+
289
+ It runs the applicable checks below and reports each one as passed, failed, or **skipped**:
290
+
291
+ | Check | What it does |
292
+ |---|---|
293
+ | `json_parses` | The file is a JSON object |
294
+ | `known_schema` | The `schema` value is one this verifier knows. An unknown schema **stops** verification rather than guessing at the meaning of a newer format |
295
+ | `canonical_bytes` | Whether the bytes are already canonical. Non-canonical bytes are re-canonicalized and reported, not failed — pretty-printing a receipt on its way through a bug tracker changes formatting, not meaning |
296
+ | `receipt_digest` | The recorded digest matches the canonicalized body with only `integrity.receipt_digest` excluded |
297
+ | `event_digest` | The embedded canonical event and every duplicated projection match the recorded event digest; disposed payloads take the documented-disposition path described above |
298
+ | `lifecycle:<event_id>` | Every embedded successor's canonical body, digest, type, and root/predecessor relationship agree |
299
+ | `integrity_attestation:<id>` | Every included attestation digest verifies and an upgraded tier has the exact verified capability it claims |
300
+ | `document:<key>@<version>` | Each supplied file hashes to the digest the receipt recorded. A document whose bytes you did not bring is reported `not_supplied` |
301
+ | `chain_linkage` | Chain fields, when present, are internally coherent. Not chained is reported as a configuration fact, not a finding |
302
+
303
+ Three states, not two, everywhere. A check that could not run is neither a pass nor a failure,
304
+ and collapsing it into either turns "we did not look" into "we looked and it was fine." A
305
+ skipped check never makes a result succeed and never makes it fail; it stays visible.
306
+
307
+ Files are matched to documents by key, version, and locale, most specific first:
308
+ `terms-2026-08-15-en.md`, then `terms-2026-08-15.md`, then `terms.en.md`, then `terms.md`, then
309
+ any file whose basename starts with `terms-`.
310
+
311
+ Exit status is `0` when every required check passed, `1` when any check failed, and `2` when no
312
+ check failed but a required document artifact was not supplied.
313
+
314
+ ---
315
+
316
+ ## What a successful verification does and does not establish
317
+
318
+ It **does** establish:
319
+
320
+ - the file is well-formed JSON in a receipt schema this verifier knows by name;
321
+ - its bytes are canonical under RFC 8785, so two readers digest the same thing;
322
+ - the digest the receipt carries matches the body it travels with, so accidental or ordinary
323
+ modification of those bytes is detected;
324
+ - the embedded event body and duplicated receipt projections match the event digest, or the
325
+ receipt contains a coherent documented core disposition instead of pretending deleted bytes
326
+ were re-derived;
327
+ - each document file you supplied hashes to the digest the receipt recorded for it; and
328
+ - any chain links present are consistent with each other.
329
+
330
+ It does **not** establish:
331
+
332
+ - **that the receipt was not fabricated.** A self-contained file verifying against itself shows
333
+ internal consistency and nothing about origin. A party who controlled the application, the
334
+ database, and the export could have produced every byte in it, including the digest, and this
335
+ verifier would say "verified" — because the only thing it can compare the bytes against is
336
+ the bytes;
337
+ - **when anything happened.** `recorded_at_by_server` is a time an application server wrote
338
+ down. It is the server's own clock, attested by nobody else;
339
+ - **who acted.** An actor reference identifies a record in somebody's database, not a person;
340
+ - **that a chain was not rewritten.** A single receipt can only show its own links are
341
+ coherent. Proving the chain itself is intact needs the neighbouring receipts and a head held
342
+ somewhere the database operator does not control;
343
+ - **that any of it is sufficient, adequately presented, or admissible anywhere.** That is not a
344
+ property of a file.
345
+
346
+ Origin and time evidence come from things this verifier cannot supply on its own: a verified
347
+ outside publication of the exact event-chain snapshot, an RFC 3161 or trust-service token over
348
+ the exact event digest, or a provider's own signed receipt. Where those exist they are reported as
349
+ exactly the assurance they supply. Where they do not, their absence is visible rather than
350
+ papered over by a check mark. Every result, in both the library and the CLI, prints that caveat
351
+ alongside the verdict.
352
+
353
+ ---
354
+
355
+ ## Exporting
356
+
357
+ ```ruby
358
+ Clickwrap.export_receipt(
359
+ receipt,
360
+ requested_by: current_operator,
361
+ because: "Investigate dispute 2026-184",
362
+ include_ip_address: false,
363
+ include_browser_user_agent: false,
364
+ include_ip_geolocation: false
365
+ )
366
+ ```
367
+
368
+ There is deliberately no `include_sensitive_context: true`. One flag that turns on three
369
+ different categories of personal data is exactly the kind of option that makes an operator's
370
+ intent unreviewable a year later.
371
+
372
+ Naming any category as `true` requires a non-empty `because:` and a `true` from
373
+ `config.authorize_unredacted_request_evidence_access_with`; the default callback returns
374
+ `false` for everyone. Every export appends a `ReceiptAccess` row recording who asked, why, and
375
+ which categories were included. Foreign event IDs return not found, so existence is not leaked.
376
+
377
+ ---
378
+
379
+ ## The golden-fixture policy
380
+
381
+ Released evidence formats are permanent. That is a stronger promise than semantic versioning,
382
+ and it has three parts the project holds itself to:
383
+
384
+ 1. **`ReceiptVerifier::KNOWN_SCHEMAS` only ever grows.** A new gem version may stop *creating*
385
+ an old schema. It must never stop *verifying* one, because receipts exported under it are
386
+ out in the world and their whole value is that they still check out years later.
387
+ 2. **A format change means a new explicit schema and a new branch of verification logic** —
388
+ never a silent reinterpretation of an old name. `CANONICAL_SCHEMA_VERSION` is deliberately
389
+ independent of the gem `VERSION`, and `VERIFIER_VERSION` moves only when receiver-side
390
+ verification changes in a way an auditor should be able to see in a receipt.
391
+ 3. **Every released receipt schema, canonicalization profile, digest field, event action, and
392
+ lifecycle meaning gets a permanent fixture**, and the suite verifies every previously
393
+ released format on every run. The fixtures live in `test/fixtures/receipts/` — real
394
+ receipts, byte for byte, exactly as an earlier version exported them — with their document
395
+ bytes beside them, and `test/golden_receipts_test.rb` runs the current verifier against all
396
+ of them. They are never regenerated. When a format legitimately changes, the correct move is
397
+ to add a fixture under the new schema name and leave the old ones alone: a fixture updated
398
+ to match new behavior has stopped testing anything.
399
+
400
+ Two consequences for contributors. Anything touching public vocabulary or an evidence claim — a
401
+ receipt field, a lifecycle meaning, a sentence a receipt prints about what it proves — needs
402
+ the documentation change alongside the code plus a note on what happens to receipts already
403
+ written under the old behavior. And released migrations are never edited underneath an
404
+ installed application; upgrades add migrations and report their exact effects.
405
+
406
+ ---
407
+
408
+ ## Sources
409
+
410
+ | Source | Class |
411
+ |---|---|
412
+ | [RFC 8785, JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785) | Technical standard |
413
+ | [NIST FIPS 180-4](https://csrc.nist.gov/pubs/fips/180-4/upd1/final) — SHA-2 is a hash standard, not a signature, identity, or time source | Technical standard |
414
+ | [RFC 3161](https://www.rfc-editor.org/info/rfc3161/) — a separate time-stamp protocol | Technical standard |
415
+ | The profile rules, the two-digest split, and the export design | Product-design inference |