Class: Clickwrap::Generators::InstallGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Includes:
ActiveRecord::Generators::Migration
Defined in:
lib/generators/clickwrap/install_generator.rb

Overview

rails generate clickwrap:install — the adaptive migration, one annotated initializer, a conventional policy file, placeholder legal content, and an optional engine mount.

Two things make this installer different from an ordinary one.

First, it asks. Recording an IP address or a provider-estimated city is a decision with consequences for the people using the host application, so every request-evidence field is a separate question in plain English with the consequence stated before the choice — never a category enabled as a side effect of something else.

Second, it refuses to guess. The actor class is inferred only when it is unambiguous, legal text is never invented, and no purpose, retention period or resolver is written on the host's behalf. Proxy provenance is derived from the effective Rails rules themselves rather than invented. An incomplete personal-data choice stops the generator before it writes a file.

The only --request-evidence-recipe is privacy-minimized, a convenient spelling of the off-by-default posture. It disappears after generation; it is deliberately not a runtime concept. There is no record_network_context, record_everything, full_evidence, or regulation-named mode switch anywhere in this gem, because a flag that enables a whole category of personal data is the thing this gem exists not to do, and no flag can make a legal determination on a host's behalf.

Constant Summary collapse

SUPPORTED_ADAPTERS =

The adapters the gem's portable core behavior is tested against.

%w[sqlite sqlite3 postgresql postgis mysql mysql2 trilogy].freeze
RECIPES =
%w[privacy-minimized].freeze
[
  { dir: "app/content/pages/legal", terms: "terms.html.md", privacy: "privacy.html.md",
    rendered_by_the_application: true },
  { dir: "app/content/legal", terms: "terms.md", privacy: "privacy.md",
    rendered_by_the_application: false }
].freeze
IP_GEOLOCATION_FIELDS =

The names the configuration uses, which are also the names this generator's one geolocation flag accepts. There used to be nine separate class_options here, spelled in the plural, plus a table translating them back into the singular configuration names — twenty-odd lines of generator surface for one list.

Clickwrap::Vocabulary::IP_GEOLOCATION_DATA_FIELDS
OPTIONAL_TABLE_MIGRATIONS =

--- Optional tables ------------------------------------------------------

Seven of the seventeen tables this gem knows about cannot receive a row until a matching configuration is turned on, and every one of those is off by default. Emitting them all anyway makes an installation's schema claim capabilities and data categories that installation does not have.

Each flag adds one migration, and re-running the installer later with the flag adds it then. clickwrap:hardening is the precedent: an opt-in generator, not a switch inside a migration nobody re-reads.

{
  with_persisted_presentations: "create_clickwrap_presentation_tables",
  with_request_evidence: "create_clickwrap_request_evidence_tables",
  with_integrity: "create_clickwrap_integrity_tables",
  with_retention_ops: "create_clickwrap_retention_tables",
  with_external_actions: "create_clickwrap_external_action_tables"
}.freeze
OPTIONAL_TABLE_SUMMARIES =

What each omitted flag would have added, in one line, for the post-install message.

{
  with_persisted_presentations:
    "--with-persisted-presentations   presentations retained before submission",
  with_request_evidence:
    "--with-request-evidence          IP address / user-agent / geolocation annex",
  with_integrity:
    "--with-integrity                 event chaining, anchoring, timestamp attestations",
  with_retention_ops:
    "--with-retention-ops             legal holds and reviewed disposition plans",
  with_external_actions:
    "--with-external-actions          the outbox for authorize_external_action!"
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.next_migration_number(dir) ⇒ Object



180
181
182
# File 'lib/generators/clickwrap/install_generator.rb', line 180

def self.next_migration_number(dir)
  ActiveRecord::Generators::Base.next_migration_number(dir)
end

Instance Method Details

#ask_about_request_evidenceObject

The questions come verbatim from the request-evidence design: each one states the consequence before asking for the choice, because a developer who has not thought about IP addresses this week deserves the context in the question rather than in a document they will read later.



218
219
220
221
222
223
224
225
226
# File 'lib/generators/clickwrap/install_generator.rb', line 218

def ask_about_request_evidence
  ask_request_evidence_questions unless skip_request_evidence_questions?
  validate_request_evidence_choices!

  # The summary prints in every mode, before any file is written, so the
  # operator sees every enabled field, purpose, encryption choice,
  # source posture, access behavior, and retention rule together.
  summarize_request_evidence_choices
end

#create_initializerObject



237
238
239
# File 'lib/generators/clickwrap/install_generator.rb', line 237

def create_initializer
  template "initializer.rb.erb", "config/initializers/clickwrap.rb"
end

Placeholders only, and only when the application has no legal text anywhere Clickwrap recognizes. Clickwrap never invents legal text: the words are the host's, reviewed by the host's counsel, and a gem that shipped plausible-looking Terms would be inviting an application to publish text nobody read.

When the app ALREADY serves legal pages (a Sitepress content directory, or a previous install's files), the generated config points at those exact files instead of writing a second set: what people accept must be the same bytes the public legal routes render, and two copies of the Terms is how they silently stop being the same document.



256
257
258
259
260
261
262
263
264
265
# File 'lib/generators/clickwrap/install_generator.rb', line 256

def create_legal_content_placeholders
  if detected_legal_documents
    say_status :found, "existing legal pages in #{detected_legal_documents[:dir]}" \
                       "config/clickwrap.rb points at them; no placeholders written", :green
    return
  end

  write_legal_placeholder "terms.md.erb", "app/content/legal/terms.md"
  write_legal_placeholder "privacy.md.erb", "app/content/legal/privacy.md"
end

#create_migration_fileObject



228
229
230
231
232
233
234
235
# File 'lib/generators/clickwrap/install_generator.rb', line 228

def create_migration_file
  migration_template "create_clickwrap_tables.rb.erb",
                     File.join(db_migrate_path, "create_clickwrap_tables.rb")

  requested_optional_table_migrations.each do |name|
    migration_template "#{name}.rb.erb", File.join(db_migrate_path, "#{name}.rb")
  end
end

#create_policy_fileObject



241
242
243
# File 'lib/generators/clickwrap/install_generator.rb', line 241

def create_policy_file
  template "clickwrap_policies.rb.erb", "config/clickwrap.rb"
end

#detect_environment!Object

Everything this run decides about the host is resolved HERE, before a single file is written, and memoized. The steps below create config/initializers/clickwrap.rb, config/clickwrap.rb, and content files; re-reading the environment halfway through would answer a different question than the one this step asked, and the post-install message would describe an application that did not exist when the decisions were made.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/generators/clickwrap/install_generator.rb', line 190

def detect_environment!
  database_adapter
  primary_key_type
  devise_detected?
  rails_authentication_detected?
  actor_class_name

  say "\n☑️  Installing clickwrap.", :green
  say "   Database adapter:  #{database_adapter || "not detected"}"
  say "   Primary key type:  #{primary_key_type_description}"
  say "   Authentication:    #{detected_authentication || "none detected (both integrations are optional)"}"
  say "   Actor class:       #{actor_class_name || "not inferred — see the initializer"}"

  return if actor_class_name

  say "\n⚠️  Clickwrap could not infer your actor class unambiguously.", :yellow
  say "   #{actor_class_reason}", :yellow
  say "   `config.actor_class_name` is therefore left COMMENTED OUT in the", :yellow
  say "   generated initializer, with an explanation beside it. Which record", :yellow
  say "   can act is a security-relevant identity mapping, and a wrong guess", :yellow
  say "   attributes evidence to the wrong kind of record for years. Set it", :yellow
  say "   yourself, or re-run with --actor-class=YourModel.", :yellow
end

#display_post_install_messageObject



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/generators/clickwrap/install_generator.rb', line 301

def display_post_install_message
  say "\n☑️  The `clickwrap` gem has been installed.", :green
  say "\nTo complete the setup:"

  step = 0
  say "  #{step += 1}. Run 'rails db:migrate' to create the clickwrap tables."
  say "     ⚠️  You must run migrations before starting your app!", :yellow
  say_optional_tables

  say "  #{step += 1}. Declare which records can act:"
  say "       class #{actor_class_name || "User"} < ApplicationRecord"
  say "         has_clickwraps"
  say "       end"
  say "       (and set `config.actor_class_name` in the initializer)" unless actor_class_name

  if detected_legal_documents
    say "  #{step += 1}. Your existing legal pages are the documents (same bytes people"
    say "     read and accept):"
    say "       #{terms_document_path}"
    say "       #{privacy_document_path}"
    if documents_needing_an_explicit_version.empty?
      say "       Each page names its own version in its front matter, so config/clickwrap.rb"
      say "       declares both without a `version:` line."
    else
      say "       A page that names its own version in its front matter is declared in"
      say "       config/clickwrap.rb without a `version:` line."
      say_documents_needing_an_explicit_version
    end
  else
    say "  #{step += 1}. Replace the placeholder legal text with your own reviewed documents:"
    say "       #{terms_document_path}"
    say "       #{privacy_document_path}"
    say "       Keep the `last_updated:` front matter at the top of each file accurate:"
    say "       that line is the version label, so a text change is one edit in one file."
  end

  say "  #{step += 1}. Publish immutable snapshots:"
  say "       bin/rails clickwrap:publish"
  say "       Deploys do this for you — publishing rides `db:prepare`, so a snapshot"
  say "       exists before the server takes traffic."

  say "  #{step += 1}. Render the policy and its bound submit action:"
  say "       <%= form.clickwrap :signup, submit: \"Create account\" %>"
  say "       That renders ONE line — a checkbox and a sentence with your documents"
  say "       linked inside it:"
  say "         [ ] I agree to the Terms and I acknowledge the Privacy Notice."
  say "       Point each document at the page people read it on, and the links in that"
  say "       sentence go there:"
  say "         Clickwrap.document :terms, from: ..., link: \"/legal/terms\""

  step = say_the_authentication_door_step(step)

  say "  #{step += 1}. Set up your test suite (presentations refuse unpublished documents"
  say "     in tests exactly as in production):"
  say "       # test/test_helper.rb"
  say "       class ActiveSupport::TestCase"
  say "         include Clickwrap::TestHelpers"
  say "         parallelize_setup { Clickwrap.publish! }   # per parallel worker..."
  say "       end"
  say "       Clickwrap.publish!                           # ...and once per process"

  unless @mounted_engine
    say "  #{step += 1}. Mount the standalone capture/receipt/withdrawal screens when you want them:"
    say "       # config/routes.rb"
    say "       mount Clickwrap::Engine => \"/agreements\""
  end

  if any_ip_geolocation_field?
    say "  #{step + 1}. Ensure #{ip_geolocation_resolver_class_name} and its data source", :yellow
    say "     are available in every environment. The initializer uses the resolver you", :yellow
    say "     explicitly selected; Clickwrap refuses to boot with fields it cannot resolve.", :yellow
  end

  print_review_checklist
end

#mount_engineObject



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/generators/clickwrap/install_generator.rb', line 267

def mount_engine
  return if options[:skip_routes]
  return say_missing_routes_file unless routes_file?
  return say_already_mounted if engine_already_mounted?
  return mount_engine_without_asking unless interactive?

  return unless ask_question(<<~QUESTION)
    Mount the Clickwrap engine at "/agreements" in config/routes.rb?
    It adds actor-owned capture, receipt, consent-withdrawal, and
    document-history screens using your parent controller, layout, locale,
    and authorization callbacks — so a required agreement can be completed
    in place instead of becoming a dead end. Nothing is exposed publicly:
    access still goes through your own authorization callbacks. [y/N]
  QUESTION

  route "mount Clickwrap::Engine => \"/agreements\""
  @mounted_engine = true
end


286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/generators/clickwrap/install_generator.rb', line 286

def print_unsupported_adapter_notes
  return if supported_adapter?

  say "\n⚠️  #{database_adapter || "Your database adapter"} is outside the set clickwrap tests.", :yellow
  say "   Tested: SQLite, PostgreSQL, and MySQL. On anything else these are", :yellow
  say "   unverified rather than known-broken, and worth checking yourself:", :yellow
  say "     • the JSON/JSONB column types and their defaults in the migration;", :yellow
  say "     • long document bodies (a silently truncated agreement is the", :yellow
  say "       worst failure this gem has);", :yellow
  say "     • whether the unique index on (policy_key, idempotency_key) really", :yellow
  say "       rejects a duplicate submit under concurrency; and", :yellow
  say "     • `rails generate clickwrap:hardening --database`, whose update and", :yellow
  say "       delete protections are written for PostgreSQL only.", :yellow
end