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.
- checksums.yaml +4 -4
- data/.rubocop.yml +90 -0
- data/CHANGELOG.md +612 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +241 -0
- data/app/controllers/clickwrap/application_controller.rb +79 -0
- data/app/controllers/clickwrap/captures_controller.rb +145 -0
- data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
- data/app/controllers/clickwrap/receipts_controller.rb +115 -0
- data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
- data/app/helpers/clickwrap/engine_helper.rb +97 -0
- data/app/views/clickwrap/captures/show.html.erb +34 -0
- data/app/views/clickwrap/receipts/index.html.erb +38 -0
- data/app/views/clickwrap/receipts/show.html.erb +91 -0
- data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
- data/app/views/clickwrap/shared/_fields.html.erb +100 -0
- data/app/views/clickwrap/shared/_statement.html.erb +105 -0
- data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
- data/config/locales/en.yml +160 -0
- data/config/locales/es.yml +138 -0
- data/config/routes.rb +41 -0
- data/exe/clickwrap +374 -0
- data/guides/README.md +30 -0
- data/guides/accessibility.md +249 -0
- data/guides/consent-and-lifecycle.md +299 -0
- data/guides/integrating.md +610 -0
- data/guides/integrity.md +212 -0
- data/guides/migrating.md +335 -0
- data/guides/naming.md +320 -0
- data/guides/organizations.md +320 -0
- data/guides/receipts-and-verification.md +415 -0
- data/guides/request-evidence.md +512 -0
- data/guides/retention-and-legal-holds.md +438 -0
- data/lib/clickwrap/actor_proxy.rb +147 -0
- data/lib/clickwrap/anonymous_actor.rb +47 -0
- data/lib/clickwrap/authority.rb +174 -0
- data/lib/clickwrap/canonical_json.rb +216 -0
- data/lib/clickwrap/capture/event_builder.rb +220 -0
- data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
- data/lib/clickwrap/capture.rb +650 -0
- data/lib/clickwrap/configuration.rb +1129 -0
- data/lib/clickwrap/controller_helpers.rb +758 -0
- data/lib/clickwrap/current_state.rb +282 -0
- data/lib/clickwrap/digest.rb +125 -0
- data/lib/clickwrap/doctor.rb +418 -0
- data/lib/clickwrap/document_definition.rb +255 -0
- data/lib/clickwrap/document_renderer.rb +83 -0
- data/lib/clickwrap/document_renderers/markdown.rb +175 -0
- data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
- data/lib/clickwrap/dsl/policy_builder.rb +462 -0
- data/lib/clickwrap/dsl/retention_builder.rb +89 -0
- data/lib/clickwrap/durable_commit_callback.rb +37 -0
- data/lib/clickwrap/engine.rb +184 -0
- data/lib/clickwrap/errors.rb +181 -0
- data/lib/clickwrap/form_builder_extensions.rb +341 -0
- data/lib/clickwrap/front_matter.rb +67 -0
- data/lib/clickwrap/identifier.rb +112 -0
- data/lib/clickwrap/import/external_receipt.rb +241 -0
- data/lib/clickwrap/import/fine_print.rb +290 -0
- data/lib/clickwrap/import/legacy.rb +450 -0
- data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
- data/lib/clickwrap/integrity/anchor.rb +130 -0
- data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
- data/lib/clickwrap/integrity/attestor.rb +221 -0
- data/lib/clickwrap/integrity/chain.rb +313 -0
- data/lib/clickwrap/integrity/timestamp.rb +143 -0
- data/lib/clickwrap/ip_geolocation/location.rb +112 -0
- data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
- data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
- data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
- data/lib/clickwrap/ip_geolocation.rb +16 -0
- data/lib/clickwrap/lifecycle.rb +534 -0
- data/lib/clickwrap/linter.rb +382 -0
- data/lib/clickwrap/localized_text.rb +101 -0
- data/lib/clickwrap/macros.rb +203 -0
- data/lib/clickwrap/models/application_record.rb +20 -0
- data/lib/clickwrap/models/chain_head.rb +79 -0
- data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
- data/lib/clickwrap/models/disposition_plan.rb +208 -0
- data/lib/clickwrap/models/document.rb +46 -0
- data/lib/clickwrap/models/document_version.rb +163 -0
- data/lib/clickwrap/models/event.rb +743 -0
- data/lib/clickwrap/models/event_document.rb +79 -0
- data/lib/clickwrap/models/event_statement.rb +92 -0
- data/lib/clickwrap/models/external_action.rb +150 -0
- data/lib/clickwrap/models/integrity_attestation.rb +90 -0
- data/lib/clickwrap/models/legal_hold.rb +81 -0
- data/lib/clickwrap/models/policy_revision.rb +115 -0
- data/lib/clickwrap/models/presentation.rb +59 -0
- data/lib/clickwrap/models/receipt_access.rb +53 -0
- data/lib/clickwrap/models/recording_sequence.rb +21 -0
- data/lib/clickwrap/models/request_evidence.rb +378 -0
- data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
- data/lib/clickwrap/models/statement_state.rb +130 -0
- data/lib/clickwrap/pending_receipt.rb +177 -0
- data/lib/clickwrap/policy.rb +283 -0
- data/lib/clickwrap/presentation_manifest.rb +210 -0
- data/lib/clickwrap/presenter.rb +716 -0
- data/lib/clickwrap/privacy.rb +419 -0
- data/lib/clickwrap/protected_outcome.rb +120 -0
- data/lib/clickwrap/receipt.rb +606 -0
- data/lib/clickwrap/receipt_html.rb +235 -0
- data/lib/clickwrap/receipt_verifier.rb +978 -0
- data/lib/clickwrap/reference.rb +44 -0
- data/lib/clickwrap/registration.rb +236 -0
- data/lib/clickwrap/registry.rb +54 -0
- data/lib/clickwrap/remediation_token.rb +155 -0
- data/lib/clickwrap/request_evidence_extractor.rb +590 -0
- data/lib/clickwrap/request_evidence_policy.rb +261 -0
- data/lib/clickwrap/retention/applier.rb +231 -0
- data/lib/clickwrap/retention/disposition.rb +221 -0
- data/lib/clickwrap/retention/planner.rb +502 -0
- data/lib/clickwrap/retention_class.rb +97 -0
- data/lib/clickwrap/reviewed_text.rb +28 -0
- data/lib/clickwrap/schema_requirements.rb +196 -0
- data/lib/clickwrap/services/authorize_external_action.rb +149 -0
- data/lib/clickwrap/services/load_policies.rb +69 -0
- data/lib/clickwrap/services/publish_documents.rb +251 -0
- data/lib/clickwrap/services/validate_policy_references.rb +166 -0
- data/lib/clickwrap/statement.rb +248 -0
- data/lib/clickwrap/subject_fingerprint.rb +28 -0
- data/lib/clickwrap/submission.rb +169 -0
- data/lib/clickwrap/system_actor.rb +31 -0
- data/lib/clickwrap/test_helpers.rb +676 -0
- data/lib/clickwrap/testing.rb +211 -0
- data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
- data/lib/clickwrap/verification.rb +504 -0
- data/lib/clickwrap/version.rb +12 -1
- data/lib/clickwrap/view_helpers.rb +190 -0
- data/lib/clickwrap/vocabulary.rb +294 -0
- data/lib/clickwrap.rb +497 -7
- data/lib/generators/clickwrap/document_generator.rb +164 -0
- data/lib/generators/clickwrap/hardening_generator.rb +177 -0
- data/lib/generators/clickwrap/install_generator.rb +1287 -0
- data/lib/generators/clickwrap/link_generator.rb +56 -0
- data/lib/generators/clickwrap/policy_generator.rb +118 -0
- data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
- data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
- data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
- data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
- data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
- data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
- data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
- data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
- data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
- data/lib/generators/clickwrap/views_generator.rb +101 -0
- data/lib/tasks/clickwrap.rake +569 -0
- metadata +311 -16
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateClickwrapTables < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# clickwrap_documents / clickwrap_document_versions
|
|
9
|
+
#
|
|
10
|
+
# A logical document (:terms) is separate from its immutable versions,
|
|
11
|
+
# because that separation is what lets a receipt from 2026 still show the
|
|
12
|
+
# exact bytes bound into an accepted server offer after the file on disk has
|
|
13
|
+
# changed a dozen times. Publishing freezes `content` and its digest; a change means a new
|
|
14
|
+
# version row, never an UPDATE. `retired_at` stops future presentation
|
|
15
|
+
# without touching history.
|
|
16
|
+
#
|
|
17
|
+
# `rendered_content` exists because "this Markdown file existed" and "this
|
|
18
|
+
# rendered representation was offered" are different claims. When a source
|
|
19
|
+
# format is transformed for display we store both, with the renderer and
|
|
20
|
+
# sanitizer identity that produced it, so neither claim borrows the other's
|
|
21
|
+
# credibility.
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
create_table :clickwrap_documents, id: primary_key_type do |t|
|
|
24
|
+
t.string :document_key, null: false
|
|
25
|
+
t.string :tenant_key
|
|
26
|
+
t.datetime :created_at, precision: 6, null: false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
add_index :clickwrap_documents, [ :tenant_key, :document_key ],
|
|
30
|
+
unique: true, name: "index_clickwrap_documents_on_tenant_and_key"
|
|
31
|
+
|
|
32
|
+
create_table :clickwrap_document_versions, id: primary_key_type do |t|
|
|
33
|
+
t.references :document, null: false, type: foreign_key_type, index: false,
|
|
34
|
+
foreign_key: { to_table: :clickwrap_documents }
|
|
35
|
+
|
|
36
|
+
t.string :version_label, null: false
|
|
37
|
+
t.string :locale, null: false, default: "en"
|
|
38
|
+
t.string :media_type, null: false, default: "text/plain"
|
|
39
|
+
|
|
40
|
+
t.send(text_column_type, :content)
|
|
41
|
+
t.bigint :content_byte_size
|
|
42
|
+
t.string :content_digest_algorithm, null: false, default: "sha256"
|
|
43
|
+
t.string :content_digest, null: false
|
|
44
|
+
|
|
45
|
+
t.send(text_column_type, :rendered_content)
|
|
46
|
+
t.string :rendered_media_type
|
|
47
|
+
t.string :rendered_content_digest
|
|
48
|
+
t.string :renderer_name
|
|
49
|
+
t.string :renderer_version
|
|
50
|
+
t.string :sanitizer_name
|
|
51
|
+
t.string :sanitizer_version
|
|
52
|
+
|
|
53
|
+
# Where the bytes live. `database` is the default and the only one that
|
|
54
|
+
# needs nothing else to stay readable; the others record a locator whose
|
|
55
|
+
# adapter must still return immutable bytes plus a verifiable digest.
|
|
56
|
+
t.string :storage_backend, null: false, default: "database"
|
|
57
|
+
t.string :storage_locator
|
|
58
|
+
t.string :source_reference
|
|
59
|
+
|
|
60
|
+
t.datetime :effective_at, precision: 6
|
|
61
|
+
t.datetime :published_at, precision: 6
|
|
62
|
+
t.datetime :retired_at, precision: 6
|
|
63
|
+
t.string :retired_reason
|
|
64
|
+
|
|
65
|
+
t.datetime :created_at, precision: 6, null: false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
add_index :clickwrap_document_versions, [ :document_id, :version_label, :locale ],
|
|
69
|
+
unique: true, name: "index_clickwrap_document_versions_on_identity"
|
|
70
|
+
add_index :clickwrap_document_versions, :content_digest,
|
|
71
|
+
name: "index_clickwrap_document_versions_on_content_digest"
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# clickwrap_policy_revisions
|
|
75
|
+
#
|
|
76
|
+
# Policies are written in Ruby, because that is reviewable in a pull request
|
|
77
|
+
# and deploys with the code. But an export must stay intelligible after the
|
|
78
|
+
# source has moved on, so the compiled snapshot is frozen here the first time
|
|
79
|
+
# a revision is offered or captured. The revision digest is the identity:
|
|
80
|
+
# change a statement's wording and you get a new revision, and old events go
|
|
81
|
+
# on pointing at the one they were captured under.
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
create_table :clickwrap_policy_revisions, id: primary_key_type do |t|
|
|
84
|
+
t.string :policy_key, null: false
|
|
85
|
+
t.string :revision_digest, null: false
|
|
86
|
+
t.send(json_column_type, :compiled_snapshot, null: false)
|
|
87
|
+
t.string :retention_class_key
|
|
88
|
+
t.string :canonical_schema_version, null: false
|
|
89
|
+
t.string :gem_version, null: false
|
|
90
|
+
t.datetime :compiled_at, precision: 6, null: false
|
|
91
|
+
t.datetime :created_at, precision: 6, null: false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
add_index :clickwrap_policy_revisions, [ :policy_key, :revision_digest ],
|
|
95
|
+
unique: true, name: "index_clickwrap_policy_revisions_on_identity"
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# clickwrap_events
|
|
99
|
+
#
|
|
100
|
+
# The append-oriented spine. Every act, correction, withdrawal, expiry,
|
|
101
|
+
# consumption, disposition, and hold is a row here, linked to what came
|
|
102
|
+
# before. Ordinary model updates are refused; fixed write sets exist for
|
|
103
|
+
# finalization, pointer nullification, annex links, holds, and reviewed
|
|
104
|
+
# disposition. There is no generic `updated_at` column that could obscure
|
|
105
|
+
# which named transition occurred.
|
|
106
|
+
#
|
|
107
|
+
# The public primary key is a ULID string: it appears verbatim in receipts
|
|
108
|
+
# without exposing a count of unrelated records. Chronology never relies on
|
|
109
|
+
# lexical ULID order. A separate database-generated sequence gives committed
|
|
110
|
+
# events one durable total order across actors and application processes.
|
|
111
|
+
#
|
|
112
|
+
# `recorded_at_by_server` is the evidentiary time, and it is named for
|
|
113
|
+
# exactly what it is: the application server's clock. `occurred_at` is
|
|
114
|
+
# separate and only set by importers, which know when something happened but
|
|
115
|
+
# were not there for it.
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Intentionally only the generated primary key: this row is ordering
|
|
118
|
+
# machinery, not a second event timestamp or a second evidence payload.
|
|
119
|
+
# This one table deliberately does NOT inherit a UUID host application's
|
|
120
|
+
# default primary-key type. Its generated numeric key is the private total
|
|
121
|
+
# order referenced by `clickwrap_events.recording_sequence`; the public
|
|
122
|
+
# event identifier remains the non-enumerable ULID.
|
|
123
|
+
create_table :clickwrap_recording_sequences, id: :primary_key do |_t|
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
create_table :clickwrap_events, id: :string, limit: 26 do |t|
|
|
127
|
+
t.string :event_type, null: false
|
|
128
|
+
t.string :policy_key, null: false
|
|
129
|
+
t.references :policy_revision, null: true, type: foreign_key_type, index: false,
|
|
130
|
+
foreign_key: { to_table: :clickwrap_policy_revisions }
|
|
131
|
+
|
|
132
|
+
# Lifecycle links. `root_event_id` points at the capture a later event
|
|
133
|
+
# acts on; `predecessor_event_id` points at the event it directly
|
|
134
|
+
# replaces. Correction, withdrawal, expiry, consumption and supersession
|
|
135
|
+
# all create linked rows — they never rewrite what they succeed.
|
|
136
|
+
t.string :root_event_id, limit: 26
|
|
137
|
+
t.string :predecessor_event_id, limit: 26
|
|
138
|
+
|
|
139
|
+
t.string :actor_type
|
|
140
|
+
t.string :actor_id
|
|
141
|
+
t.string :actor_reference, null: false
|
|
142
|
+
t.send(json_column_type, :actor_snapshot, default: json_column_default)
|
|
143
|
+
|
|
144
|
+
# Who the actor was acting for, kept as a separate fact from who they are.
|
|
145
|
+
t.string :represented_party_type
|
|
146
|
+
t.string :represented_party_id
|
|
147
|
+
t.string :represented_party_reference, null: false, default: ""
|
|
148
|
+
t.string :authority_source
|
|
149
|
+
t.string :authority_role
|
|
150
|
+
t.datetime :authority_verified_at, precision: 6
|
|
151
|
+
t.send(json_column_type, :authority_details, default: json_column_default)
|
|
152
|
+
|
|
153
|
+
t.string :tenant_key
|
|
154
|
+
t.string :subject_type
|
|
155
|
+
t.string :subject_id
|
|
156
|
+
t.string :subject_key, null: false, default: ""
|
|
157
|
+
t.string :subject_fingerprint
|
|
158
|
+
|
|
159
|
+
t.string :capture_channel, null: false
|
|
160
|
+
t.string :authentication_method
|
|
161
|
+
t.send(json_column_type, :authentication_context, default: json_column_default)
|
|
162
|
+
t.string :attribution_method, null: false, default: "unknown"
|
|
163
|
+
|
|
164
|
+
t.datetime :recorded_at_by_server, precision: 6, null: false
|
|
165
|
+
t.datetime :occurred_at, precision: 6
|
|
166
|
+
t.bigint :recording_sequence, null: false
|
|
167
|
+
|
|
168
|
+
t.string :idempotency_key
|
|
169
|
+
t.string :http_request_id
|
|
170
|
+
t.string :http_route_name
|
|
171
|
+
|
|
172
|
+
# The column is always here; the foreign key arrives with the table, in
|
|
173
|
+
# the --with-persisted-presentations migration. A default install writes
|
|
174
|
+
# nothing on GET, so it has nothing for this column to point at.
|
|
175
|
+
t.references :presentation, null: true, type: foreign_key_type, index: false
|
|
176
|
+
t.send(json_column_type, :presentation_manifest)
|
|
177
|
+
t.string :presentation_manifest_digest
|
|
178
|
+
|
|
179
|
+
# The optional personal request evidence lives in its own table so it can
|
|
180
|
+
# be deleted on its own schedule without rewriting this row. What stays
|
|
181
|
+
# here is a keyed digest binding the two together — described honestly as
|
|
182
|
+
# a retained linkable digest, never as anonymization.
|
|
183
|
+
t.references :request_evidence, null: true, type: foreign_key_type, index: false
|
|
184
|
+
t.send(json_column_type, :request_evidence_category_binding_digests,
|
|
185
|
+
null: false, default: json_column_default)
|
|
186
|
+
t.string :request_evidence_digest_algorithm
|
|
187
|
+
t.string :request_evidence_key_id
|
|
188
|
+
|
|
189
|
+
t.send(json_column_type, :protected_outcome)
|
|
190
|
+
|
|
191
|
+
t.string :provider_name
|
|
192
|
+
t.string :provider_event_id
|
|
193
|
+
t.send(json_column_type, :provider_receipt)
|
|
194
|
+
t.send(json_column_type, :provider_verification)
|
|
195
|
+
|
|
196
|
+
t.text :reason
|
|
197
|
+
|
|
198
|
+
t.string :retention_class_key
|
|
199
|
+
t.datetime :retain_core_event_until, precision: 6
|
|
200
|
+
t.string :retention_rule_name
|
|
201
|
+
t.datetime :core_event_disposed_at, precision: 6
|
|
202
|
+
t.string :core_event_disposition_event_id, limit: 26
|
|
203
|
+
t.boolean :on_legal_hold, null: false, default: false
|
|
204
|
+
|
|
205
|
+
t.string :chain_scope
|
|
206
|
+
t.bigint :chain_sequence
|
|
207
|
+
t.string :previous_event_digest
|
|
208
|
+
t.string :event_digest
|
|
209
|
+
t.string :digest_algorithm, null: false, default: "sha256"
|
|
210
|
+
t.string :canonical_schema_version, null: false
|
|
211
|
+
|
|
212
|
+
t.string :gem_version, null: false
|
|
213
|
+
t.string :application_version
|
|
214
|
+
t.string :template_version
|
|
215
|
+
|
|
216
|
+
t.datetime :created_at, precision: 6, null: false
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The idempotency guarantee is a database constraint, not application logic:
|
|
220
|
+
# a duplicate submit loses the INSERT race and gets the original receipt
|
|
221
|
+
# back rather than running the protected action a second time.
|
|
222
|
+
add_index :clickwrap_events, [ :policy_key, :idempotency_key ],
|
|
223
|
+
unique: true, name: "index_clickwrap_events_on_idempotency_key"
|
|
224
|
+
add_index :clickwrap_events, [ :chain_scope, :chain_sequence ],
|
|
225
|
+
unique: true, name: "index_clickwrap_events_on_chain_position"
|
|
226
|
+
add_index :clickwrap_events, :recording_sequence,
|
|
227
|
+
unique: true, name: "index_clickwrap_events_on_recording_order"
|
|
228
|
+
add_foreign_key :clickwrap_events, :clickwrap_recording_sequences,
|
|
229
|
+
column: :recording_sequence
|
|
230
|
+
add_index :clickwrap_events, [ :actor_reference, :policy_key ],
|
|
231
|
+
name: "index_clickwrap_events_on_actor_and_policy"
|
|
232
|
+
add_index :clickwrap_events, [ :subject_type, :subject_id ],
|
|
233
|
+
name: "index_clickwrap_events_on_subject"
|
|
234
|
+
add_index :clickwrap_events, :root_event_id, name: "index_clickwrap_events_on_root_event"
|
|
235
|
+
add_index :clickwrap_events, :predecessor_event_id,
|
|
236
|
+
name: "index_clickwrap_events_on_predecessor_event"
|
|
237
|
+
add_index :clickwrap_events, :presentation_id,
|
|
238
|
+
name: "index_clickwrap_events_on_presentation"
|
|
239
|
+
add_index :clickwrap_events, :request_evidence_id, unique: true,
|
|
240
|
+
name: "index_clickwrap_events_on_request_evidence"
|
|
241
|
+
add_index :clickwrap_events, :core_event_disposition_event_id,
|
|
242
|
+
name: "index_clickwrap_events_on_core_disposition"
|
|
243
|
+
add_index :clickwrap_events, :recorded_at_by_server,
|
|
244
|
+
name: "index_clickwrap_events_on_recorded_at_by_server"
|
|
245
|
+
add_index :clickwrap_events, :retain_core_event_until,
|
|
246
|
+
name: "index_clickwrap_events_on_retain_core_event_until"
|
|
247
|
+
add_index :clickwrap_events, :retention_class_key,
|
|
248
|
+
name: "index_clickwrap_events_on_retention_class_key"
|
|
249
|
+
add_index :clickwrap_events, :subject_key,
|
|
250
|
+
name: "index_clickwrap_events_on_subject_key"
|
|
251
|
+
add_clickwrap_check_constraint :clickwrap_events,
|
|
252
|
+
"event_type IN (#{quoted_values(%w[capture withdrawal correction supersession expiry consumption revocation renewal scope_change exemption imported_legacy external_receipt disposition legal_hold_placed legal_hold_released receipt_access provider_outcome])})",
|
|
253
|
+
name: "chk_clickwrap_events_event_type"
|
|
254
|
+
add_clickwrap_check_constraint :clickwrap_events,
|
|
255
|
+
"capture_channel IN (#{quoted_values(%w[web_browser native_app api_client operator background_job imported_provider system])})",
|
|
256
|
+
name: "chk_clickwrap_events_channel"
|
|
257
|
+
add_clickwrap_check_constraint :clickwrap_events,
|
|
258
|
+
"attribution_method IN (#{quoted_values(%w[authenticated_session account_registration public_form operator_session api_credential anonymous_identifier system_process imported_provider unknown])})",
|
|
259
|
+
name: "chk_clickwrap_events_attribution"
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# clickwrap_event_statements
|
|
263
|
+
#
|
|
264
|
+
# One row per act inside a capture. A policy with "agree to the Terms" and
|
|
265
|
+
# "acknowledge the Privacy Notice" produces one event and two rows here, and
|
|
266
|
+
# they never merge: each keeps its own kind, its own assertion text as it was
|
|
267
|
+
# resolved into the accepted server offer, its own answer, and its own validity.
|
|
268
|
+
#
|
|
269
|
+
# These rows are immutable snapshots. Current state lives in the projection
|
|
270
|
+
# table below, which can be rebuilt from these at any time.
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
create_table :clickwrap_event_statements, id: primary_key_type do |t|
|
|
273
|
+
t.string :event_id, limit: 26, null: false
|
|
274
|
+
t.integer :ordinal, null: false, default: 0
|
|
275
|
+
|
|
276
|
+
t.string :statement_key, null: false
|
|
277
|
+
t.string :kind, null: false
|
|
278
|
+
t.string :action, null: false
|
|
279
|
+
|
|
280
|
+
t.text :assertion_text, null: false
|
|
281
|
+
t.string :assertion_locale, null: false
|
|
282
|
+
t.text :label_text
|
|
283
|
+
t.send(json_column_type, :link_labels, default: json_column_default)
|
|
284
|
+
t.send(json_column_type, :choices)
|
|
285
|
+
|
|
286
|
+
t.boolean :required, null: false, default: true
|
|
287
|
+
t.boolean :optional, null: false, default: false
|
|
288
|
+
t.send(json_column_type, :answer)
|
|
289
|
+
t.boolean :answered, null: false, default: false
|
|
290
|
+
|
|
291
|
+
t.string :purpose_key
|
|
292
|
+
t.string :withdrawal_path
|
|
293
|
+
|
|
294
|
+
t.datetime :valid_from, precision: 6
|
|
295
|
+
t.datetime :expires_at, precision: 6
|
|
296
|
+
t.boolean :one_time, null: false, default: false
|
|
297
|
+
t.send(json_column_type, :requires, default: json_array_default)
|
|
298
|
+
|
|
299
|
+
t.string :subject_fingerprint
|
|
300
|
+
|
|
301
|
+
t.datetime :created_at, precision: 6, null: false
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
add_index :clickwrap_event_statements, [ :event_id, :statement_key ],
|
|
305
|
+
unique: true, name: "index_clickwrap_event_statements_on_identity"
|
|
306
|
+
add_index :clickwrap_event_statements, [ :kind, :statement_key ],
|
|
307
|
+
name: "index_clickwrap_event_statements_on_kind_and_key"
|
|
308
|
+
add_clickwrap_check_constraint :clickwrap_event_statements,
|
|
309
|
+
"kind IN (#{quoted_values(%w[agreement acknowledgment consent declaration attestation authorization])})",
|
|
310
|
+
name: "chk_clickwrap_statements_kind"
|
|
311
|
+
add_clickwrap_check_constraint :clickwrap_event_statements,
|
|
312
|
+
"action IN (#{quoted_values(%w[agreed superseded acknowledged expired granted declined withdrawn renewed scope_changed declared corrected attested authorized consumed revoked])})",
|
|
313
|
+
name: "chk_clickwrap_statements_action"
|
|
314
|
+
# Captured controls are required XOR optional. System lifecycle statements
|
|
315
|
+
# describe a transition and are therefore neither; no statement may be both.
|
|
316
|
+
add_clickwrap_check_constraint :clickwrap_event_statements, "NOT (required AND optional)",
|
|
317
|
+
name: "chk_clickwrap_statements_requirement"
|
|
318
|
+
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
# clickwrap_event_documents
|
|
321
|
+
#
|
|
322
|
+
# Exactly which document versions were bound to which statement, with the
|
|
323
|
+
# digests as they stood at capture. Verification recomputes them, so a
|
|
324
|
+
# document row edited in place is detected rather than believed.
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
create_table :clickwrap_event_documents, id: primary_key_type do |t|
|
|
327
|
+
t.string :event_id, limit: 26, null: false
|
|
328
|
+
t.string :statement_key, null: false
|
|
329
|
+
t.string :document_key, null: false
|
|
330
|
+
t.references :document_version, null: true, type: foreign_key_type, index: false,
|
|
331
|
+
foreign_key: { to_table: :clickwrap_document_versions }
|
|
332
|
+
|
|
333
|
+
t.string :version_label, null: false
|
|
334
|
+
t.string :locale, null: false
|
|
335
|
+
t.string :source_media_type
|
|
336
|
+
t.string :source_content_digest, null: false
|
|
337
|
+
t.string :rendered_media_type, null: false
|
|
338
|
+
t.string :rendered_content_digest
|
|
339
|
+
t.string :renderer_name
|
|
340
|
+
t.string :renderer_version
|
|
341
|
+
t.string :sanitizer_name
|
|
342
|
+
t.string :sanitizer_version
|
|
343
|
+
t.integer :ordinal, null: false, default: 0
|
|
344
|
+
|
|
345
|
+
t.datetime :created_at, precision: 6, null: false
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
add_index :clickwrap_event_documents, [ :event_id, :statement_key, :document_key ],
|
|
349
|
+
unique: true, name: "index_clickwrap_event_documents_on_identity"
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# clickwrap_statement_states
|
|
353
|
+
#
|
|
354
|
+
# The current-state projection: the answer to "does this person currently
|
|
355
|
+
# have X?" without walking the whole event history on every request. It is a
|
|
356
|
+
# cache of a computation over retained event payloads and can be rebuilt
|
|
357
|
+
# while the identity-bearing root payloads remain available.
|
|
358
|
+
#
|
|
359
|
+
# The unique index is doing real work: it is what stops two concurrent
|
|
360
|
+
# submits from producing two live grants or two usable authorizations for
|
|
361
|
+
# the same actor and subject. The `_key` columns exist for that index —
|
|
362
|
+
# NULLs do not collide in a unique index on most adapters, so the empty
|
|
363
|
+
# string stands in for "no tenant" and "no subject".
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
create_table :clickwrap_statement_states, id: primary_key_type do |t|
|
|
366
|
+
# A digest of the six identity columns below, carrying the unique index.
|
|
367
|
+
#
|
|
368
|
+
# This is a portability decision with teeth. Those six hold a policy key,
|
|
369
|
+
# a statement key, a GlobalID-shaped actor reference, a tenant key, a
|
|
370
|
+
# subject key, and a represented-party reference; indexing the strings
|
|
371
|
+
# directly can exceed MySQL's 3072-byte index limit
|
|
372
|
+
# under utf8mb4 even at a reduced length, so a composite unique index over
|
|
373
|
+
# them cannot be created there at all. The guarantee this index provides is
|
|
374
|
+
# the one that keeps a double submit from becoming two debits, so it has to
|
|
375
|
+
# exist on every supported database — hence one fixed-width column, with an
|
|
376
|
+
# ordinary lookup index over the readable ones beside it.
|
|
377
|
+
t.string :identity_digest, null: false, limit: 71
|
|
378
|
+
|
|
379
|
+
t.string :policy_key, null: false
|
|
380
|
+
t.string :statement_key, null: false
|
|
381
|
+
t.string :kind, null: false
|
|
382
|
+
t.string :purpose_key
|
|
383
|
+
|
|
384
|
+
t.string :actor_type
|
|
385
|
+
t.string :actor_id
|
|
386
|
+
t.string :actor_reference, null: false
|
|
387
|
+
t.string :tenant_key, null: false, default: ""
|
|
388
|
+
t.string :subject_type
|
|
389
|
+
t.string :subject_id
|
|
390
|
+
t.string :subject_key, null: false, default: ""
|
|
391
|
+
t.string :represented_party_reference, null: false, default: ""
|
|
392
|
+
t.string :subject_fingerprint
|
|
393
|
+
|
|
394
|
+
t.string :state, null: false
|
|
395
|
+
t.string :current_action, null: false
|
|
396
|
+
t.string :current_event_id, limit: 26, null: false
|
|
397
|
+
t.string :root_event_id, limit: 26
|
|
398
|
+
t.references :policy_revision, null: true, type: foreign_key_type, index: false,
|
|
399
|
+
foreign_key: { to_table: :clickwrap_policy_revisions }
|
|
400
|
+
|
|
401
|
+
t.datetime :effective_at, precision: 6, null: false
|
|
402
|
+
t.datetime :expires_at, precision: 6
|
|
403
|
+
t.datetime :withdrawn_at, precision: 6
|
|
404
|
+
t.datetime :superseded_at, precision: 6
|
|
405
|
+
t.datetime :consumed_at, precision: 6
|
|
406
|
+
t.datetime :revoked_at, precision: 6
|
|
407
|
+
t.datetime :corrected_at, precision: 6
|
|
408
|
+
t.boolean :one_time, null: false, default: false
|
|
409
|
+
|
|
410
|
+
t.send(json_column_type, :document_version_ids, default: json_array_default)
|
|
411
|
+
|
|
412
|
+
t.timestamps precision: 6
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
add_index :clickwrap_statement_states, :identity_digest,
|
|
416
|
+
unique: true, name: "index_clickwrap_statement_states_on_identity"
|
|
417
|
+
add_index :clickwrap_statement_states,
|
|
418
|
+
[ :policy_key, :statement_key ],
|
|
419
|
+
name: "index_clickwrap_statement_states_on_policy_and_statement"
|
|
420
|
+
add_index :clickwrap_statement_states, [ :actor_reference, :state ],
|
|
421
|
+
name: "index_clickwrap_statement_states_on_actor_and_state"
|
|
422
|
+
add_index :clickwrap_statement_states, :expires_at,
|
|
423
|
+
name: "index_clickwrap_statement_states_on_expires_at"
|
|
424
|
+
add_index :clickwrap_statement_states, :current_event_id,
|
|
425
|
+
name: "index_clickwrap_statement_states_on_current_event"
|
|
426
|
+
add_index :clickwrap_statement_states, :root_event_id,
|
|
427
|
+
name: "index_clickwrap_statement_states_on_root_event"
|
|
428
|
+
add_clickwrap_check_constraint :clickwrap_statement_states,
|
|
429
|
+
"state IN (#{quoted_values(%w[active declined withdrawn expired superseded consumed revoked corrected exempted])})",
|
|
430
|
+
name: "chk_clickwrap_states_state"
|
|
431
|
+
|
|
432
|
+
# A row exists before or after the first StatementState for an identity, so
|
|
433
|
+
# concurrent first-time authorizations have something portable to lock.
|
|
434
|
+
# These are coordination rows, not evidence, and contain only the canonical
|
|
435
|
+
# identity digest already used by StatementState's unique index.
|
|
436
|
+
create_table :clickwrap_statement_identity_locks, id: primary_key_type do |t|
|
|
437
|
+
t.string :identity_digest, null: false, limit: 71
|
|
438
|
+
t.datetime :created_at, precision: 6, null: false
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
add_index :clickwrap_statement_identity_locks, :identity_digest, unique: true,
|
|
442
|
+
name: "index_clickwrap_statement_identity_locks_on_identity"
|
|
443
|
+
|
|
444
|
+
# ---------------------------------------------------------------------------
|
|
445
|
+
# clickwrap_receipt_accesses
|
|
446
|
+
#
|
|
447
|
+
# Who read what, and why. Unredacted request evidence needs host
|
|
448
|
+
# authorization plus a human-readable reason, and asking for it appends a
|
|
449
|
+
# row here. An access log that nobody can read is not much of a control, so
|
|
450
|
+
# this table is plain and queryable.
|
|
451
|
+
# ---------------------------------------------------------------------------
|
|
452
|
+
create_table :clickwrap_receipt_accesses, id: primary_key_type do |t|
|
|
453
|
+
t.string :event_id, limit: 26, null: false
|
|
454
|
+
t.string :requested_by_reference
|
|
455
|
+
t.text :reason
|
|
456
|
+
t.send(json_column_type, :included_fields, null: false)
|
|
457
|
+
t.string :access_channel, null: false, default: "api"
|
|
458
|
+
t.datetime :accessed_at, precision: 6, null: false
|
|
459
|
+
t.datetime :created_at, precision: 6, null: false
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
add_index :clickwrap_receipt_accesses, [ :event_id, :accessed_at ],
|
|
463
|
+
name: "index_clickwrap_receipt_accesses_on_event"
|
|
464
|
+
|
|
465
|
+
# Every non-polymorphic evidence link is backed by the database, including
|
|
466
|
+
# lifecycle links and the tables whose ids are ULID strings rather than the
|
|
467
|
+
# host application's primary-key type. Model callbacks make ordinary writes
|
|
468
|
+
# readable; these constraints keep console SQL, bulk imports, and future code
|
|
469
|
+
# from manufacturing orphaned evidence by accident.
|
|
470
|
+
add_clickwrap_foreign_key :clickwrap_events, :clickwrap_events,
|
|
471
|
+
column: :root_event_id, name: "fk_clickwrap_events_root"
|
|
472
|
+
add_clickwrap_foreign_key :clickwrap_events, :clickwrap_events,
|
|
473
|
+
column: :predecessor_event_id, name: "fk_clickwrap_events_predecessor"
|
|
474
|
+
add_clickwrap_foreign_key :clickwrap_events, :clickwrap_events,
|
|
475
|
+
column: :core_event_disposition_event_id, name: "fk_clickwrap_events_disposition"
|
|
476
|
+
|
|
477
|
+
add_clickwrap_foreign_key :clickwrap_event_statements, :clickwrap_events,
|
|
478
|
+
column: :event_id, name: "fk_clickwrap_statements_event"
|
|
479
|
+
add_clickwrap_foreign_key :clickwrap_event_documents, :clickwrap_events,
|
|
480
|
+
column: :event_id, name: "fk_clickwrap_documents_event"
|
|
481
|
+
add_clickwrap_foreign_key :clickwrap_statement_states, :clickwrap_events,
|
|
482
|
+
column: :current_event_id, name: "fk_clickwrap_states_current_event"
|
|
483
|
+
add_clickwrap_foreign_key :clickwrap_statement_states, :clickwrap_events,
|
|
484
|
+
column: :root_event_id, name: "fk_clickwrap_states_root_event"
|
|
485
|
+
add_clickwrap_foreign_key :clickwrap_receipt_accesses, :clickwrap_events,
|
|
486
|
+
column: :event_id, name: "fk_clickwrap_receipt_accesses_event"
|
|
487
|
+
|
|
488
|
+
# The database rejects contradictory statement requirement flags and values
|
|
489
|
+
# outside this release's frozen vocabularies. A future release that adds a
|
|
490
|
+
# lifecycle value ships an upgrade migration that widens the matching check;
|
|
491
|
+
# raw SQL must not be able to manufacture evidence the model cannot read.
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
private
|
|
495
|
+
|
|
496
|
+
# Honor the host's configured primary key type (uuid vs bigint). Reads the
|
|
497
|
+
# same setting `rails g model` uses, so an app generated with
|
|
498
|
+
# `config.generators { |g| g.orm :active_record, primary_key_type: :uuid }`
|
|
499
|
+
# gets uuid clickwrap tables and uuid foreign keys, automatically.
|
|
500
|
+
#
|
|
501
|
+
# Note that clickwrap_events keeps a ULID string key regardless: its id is
|
|
502
|
+
# quoted verbatim in receipts and exports, so it has to be stable, sortable,
|
|
503
|
+
# and identical in every host.
|
|
504
|
+
def primary_and_foreign_key_types
|
|
505
|
+
config = Rails.configuration.generators
|
|
506
|
+
setting = config.options[config.orm][:primary_key_type]
|
|
507
|
+
primary_key_type = setting || :primary_key
|
|
508
|
+
foreign_key_type = setting || :bigint
|
|
509
|
+
[ primary_key_type, foreign_key_type ]
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def json_column_type
|
|
513
|
+
return :jsonb if connection.adapter_name.downcase.match?(/postg/) # postgresql, postgis
|
|
514
|
+
|
|
515
|
+
:json
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# MySQL 8+ doesn't allow default values on JSON columns. Returns an empty-hash
|
|
519
|
+
# default for SQLite/PostgreSQL, nil for MySQL. The models handle nil
|
|
520
|
+
# gracefully by defaulting to {} in their accessors.
|
|
521
|
+
def json_column_default
|
|
522
|
+
return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
|
|
523
|
+
|
|
524
|
+
{}
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Same MySQL caveat as `json_column_default`, but for list-shaped columns.
|
|
528
|
+
def json_array_default
|
|
529
|
+
return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
|
|
530
|
+
|
|
531
|
+
[]
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Legal documents are routinely longer than MySQL's 64 KB TEXT limit, and a
|
|
535
|
+
# silently truncated agreement is the worst possible failure for this gem.
|
|
536
|
+
# `:mediumtext` maps to MEDIUMTEXT on MySQL (16 MB) and to ordinary TEXT
|
|
537
|
+
# everywhere else.
|
|
538
|
+
def text_column_type
|
|
539
|
+
return :mediumtext if connection.adapter_name.downcase.match?(/mysql|trilogy/)
|
|
540
|
+
|
|
541
|
+
:text
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def quoted_values(values)
|
|
545
|
+
values.map { |value| connection.quote(value) }.join(", ")
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# SQLite implements both foreign keys and check constraints by rebuilding a
|
|
549
|
+
# table. During a long install migration, Active Record can otherwise rebuild
|
|
550
|
+
# from a schema-cache entry captured before the indexes/columns immediately
|
|
551
|
+
# above were added. Refreshing around every rebuild keeps the SQLite result
|
|
552
|
+
# identical to PostgreSQL/MySQL instead of quietly resurrecting stale shape.
|
|
553
|
+
def add_clickwrap_check_constraint(table, expression, **options)
|
|
554
|
+
refresh_clickwrap_table_schema!(table)
|
|
555
|
+
add_check_constraint(table, expression, **options)
|
|
556
|
+
refresh_clickwrap_table_schema!(table)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def add_clickwrap_foreign_key(from_table, to_table, **options)
|
|
560
|
+
refresh_clickwrap_table_schema!(from_table)
|
|
561
|
+
add_foreign_key(from_table, to_table, **options)
|
|
562
|
+
refresh_clickwrap_table_schema!(from_table)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def refresh_clickwrap_table_schema!(table)
|
|
566
|
+
connection.schema_cache.clear_data_source_cache!(table.to_s)
|
|
567
|
+
end
|
|
568
|
+
end
|