agreements 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbb4e3cb21a8e743ea9fab48f117242cad49d78e752258bbb992894498355be4
4
- data.tar.gz: f2160a49ff3799851018060c773643c9b829bad4a1d3e22944913cef62dae501
3
+ metadata.gz: 921978d679f2c21c42fda41e4caa14e844e5fbfbb57a10c78738811441814547
4
+ data.tar.gz: 342464faa1422920f63ca881f7d64728d35e6b88c7e595d351a06c181a7692cd
5
5
  SHA512:
6
- metadata.gz: 70299f73fe7bc2b6760aae7b1389fa3913298175554ac001963270372c97af651bec611613ae51d367a320dd6d785b154517cccc1c468eece83f7b25e3c0212c
7
- data.tar.gz: 7a27e58cf17b1068ecee2705f510f38f6900829eb77c5177b8a77237cb19be6e581b728f8a2eba56f973e8850977fa72e612af28dbdbafd1d787d11d86ad2e60
6
+ metadata.gz: c1cf67625ad815b0e3df2033a609cd510cf9c4ca97888b518c842546bb174b3c22abfd7bfc2670d0756337c3e24465b2e70b4a52887dbbf7e0a1a9c7c78a939b
7
+ data.tar.gz: 60cd61c042671c9bcb86f1e2f3c37ceeba9bfa2600aee3082766fe8b2124a38a7c81bc3b7ae5328f65f6265d6ac8ac6ef2f77416cd3d28547fd1c70bb47607a3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-11-12
4
+
5
+ - Add an optional `effective_at` to versions, so a version can be inserted when
6
+ notice is given and become current on the date it takes effect. Requires a
7
+ new nullable column; see "Upgrading from 0.3" in the README.
8
+ - Add `Agreements.upcoming_version` to show the next scheduled version.
9
+ - Add `Agreements::TestHelper#accept_agreement` for host test suites.
10
+ - Explain in the README what acceptance evidence does and does not prove.
11
+
12
+ ## 0.3.0 - 2026-11-12
13
+
14
+ - Answer pending non-HTML requests with `403 Forbidden` instead of letting them
15
+ through. `require_agreement` returns `:forbidden` for them.
16
+ - Reject `/\host` return paths, using Rails' `url_from`.
17
+ - Accept an optional `key` on document references for translated link titles.
18
+ - Depend on Action Pack, Active Record, Railties and Global ID instead of all
19
+ of Rails.
20
+ - Document the safe data-migration pattern, Global ID app pinning and a `nil`
21
+ `current_version` on `VersionNotCurrent`.
22
+
3
23
  ## 0.2.0 - 2026-08-27
4
24
 
5
25
  - Record the exact localized acceptance statement and locale shown by the host.
data/README.md CHANGED
@@ -29,6 +29,23 @@ view, initializer, JavaScript, CSS, or admin dashboard to integrate.
29
29
 
30
30
  Ruby >= 3.2 · Rails >= 7.1 and < 9 · PostgreSQL and SQLite
31
31
 
32
+ ### Upgrading from 0.3
33
+
34
+ Version 0.4 adds a nullable `effective_at` column to `agreements_versions`:
35
+
36
+ ```bash
37
+ bin/rails generate migration AddEffectiveAtToAgreementsVersions effective_at:datetime
38
+ bin/rails db:migrate
39
+ ```
40
+
41
+ Existing rows keep `effective_at` empty and stay in effect from their creation.
42
+
43
+ ### Upgrading from 0.2
44
+
45
+ Version 0.3 has no schema change. `require_agreement` now answers a pending
46
+ non-HTML request with `403 Forbidden` instead of letting it through. If your
47
+ API already checks `pending_version` before calling it, nothing changes.
48
+
32
49
  ### Upgrading from 0.1
33
50
 
34
51
  Version 0.2 adds non-null `acceptance_statement` and `locale` columns to
@@ -38,29 +55,92 @@ locale such as `und` when the displayed locale is unknown.
38
55
 
39
56
  ## Define a version
40
57
 
41
- Agreement versions are deployed data. Create them in a data migration and keep
42
- the same registry in your seeds for fresh databases:
58
+ Agreement versions are deployed data. Insert them in a data migration, and keep
59
+ the same list in your seeds so fresh databases match production:
43
60
 
44
61
  ```ruby
45
- Agreements::Version.create!(
62
+ class AddUserTerms20260816 < ActiveRecord::Migration[8.1]
63
+ class Version < ActiveRecord::Base
64
+ self.table_name = "agreements_versions"
65
+ end
66
+
67
+ VERSION = {
68
+ agreement_key: "user_terms",
69
+ version: "2026-08-16",
70
+ acceptance_statement: "I accept the Terms of Service and acknowledge the Privacy Notice.",
71
+ documents: [
72
+ { key: "terms_of_service", title: "Terms of Service", url: "https://example.com/terms-2026-08-16" },
73
+ { key: "privacy_notice", title: "Privacy Notice", url: "https://example.com/privacy-2026-08-16" }
74
+ ]
75
+ }.freeze
76
+
77
+ def up
78
+ version = Version.find_or_create_by!(VERSION.slice(:agreement_key, :version)) do |record|
79
+ record.assign_attributes(VERSION)
80
+ end
81
+
82
+ unless version.acceptance_statement == VERSION[:acceptance_statement] &&
83
+ version.documents == VERSION[:documents].as_json
84
+ raise "#{version.agreement_key} #{version.version} already exists with different content"
85
+ end
86
+ end
87
+
88
+ def down
89
+ raise ActiveRecord::IrreversibleMigration, "Agreement versions are evidence"
90
+ end
91
+ end
92
+ ```
93
+
94
+ The migration defines its own model so it keeps working when the gem's model
95
+ changes later. The final check stops a deploy if a version label already exists
96
+ with different content: a deployed version must never change. Use the same
97
+ create-then-check loop in your seeds.
98
+
99
+ A version without `effective_at` is current as soon as it is inserted. There is
100
+ no draft, publish or activate step. A correction or legal update is a new
101
+ immutable row with a new version label.
102
+
103
+ ### Give notice before a version takes effect
104
+
105
+ Terms often promise notice before a change applies, for example "at least 14
106
+ days before the changes take effect". Insert the version when you send the
107
+ notice, with the date it takes effect:
108
+
109
+ ```ruby
110
+ VERSION = {
46
111
  agreement_key: "user_terms",
47
- version: "2026-08-16",
48
- acceptance_statement: "I accept the Terms of Service and acknowledge the Privacy Notice.",
49
- documents: [
50
- { title: "Terms of Service", url: "https://example.com/terms" },
51
- { title: "Privacy Notice", url: "https://example.com/privacy" }
52
- ]
53
- )
112
+ version: "2026-12-01",
113
+ effective_at: Time.utc(2026, 12, 1),
114
+ # acceptance_statement, documents ...
115
+ }.freeze
54
116
  ```
55
117
 
56
- The newest inserted row for an agreement key is current immediately. There is
57
- no draft, publish, activate, or scheduling lifecycle. A correction or legal
58
- update is a new immutable row with a new version label.
118
+ Until that time the previous version stays current, nobody is asked to accept,
119
+ and `accept!` refuses the new version. From that time the new version is
120
+ current and every subject who has not accepted it is pending. Show the notice
121
+ in your app with:
122
+
123
+ ```ruby
124
+ Agreements.upcoming_version("user_terms") # => the next scheduled version, or nil
125
+ ```
59
126
 
60
- Document references accept `title`, an HTTPS `url`, and an optional 64-character
61
- `sha256`. The gem validates a supplied digest but does not fetch remote pages,
62
- calculate hashes, or retain document bytes. Hashing and archival belong in the
63
- trusted legal-document publishing process.
127
+ The current version is the one that took effect most recently: its
128
+ `effective_at`, or its creation time when `effective_at` is empty. When two
129
+ versions take effect at the same time, the one inserted later wins.
130
+
131
+ Subjects cannot accept a scheduled version early. Someone who accepts the
132
+ current version during the notice period is asked again on the effective date.
133
+
134
+ A scheduled version cannot have acceptances before it takes effect. To cancel
135
+ it, delete it in a data migration with a migration-local model before its
136
+ effective date. After that date it is evidence like any other version.
137
+
138
+ Document references accept `title`, an HTTPS `url`, an optional 64-character
139
+ `sha256`, and an optional `key`. Use `key` (lowercase letters, digits and
140
+ underscores) to look up a translated link title, so your views do not depend on
141
+ the English `title`. The gem validates a supplied digest but does not fetch
142
+ remote pages, calculate hashes, or retain document bytes. Hashing and archival
143
+ belong in the trusted legal-document publishing process.
64
144
 
65
145
  ## Ask what is pending
66
146
 
@@ -73,6 +153,19 @@ Subjects and actors may be records responding to `to_global_id`, or explicit
73
153
  non-empty opaque strings. The gem stores only the resulting keys; it does not
74
154
  own authentication, tenancy, roles, or authorization.
75
155
 
156
+ A Global ID contains your application name and the model class, for example
157
+ `gid://my-app/User/1`. If either changes, old acceptances no longer match and
158
+ every subject is asked to accept again. Pin the application name now so a
159
+ module rename cannot change it:
160
+
161
+ ```ruby
162
+ # config/application.rb
163
+ config.global_id.app = "my-app"
164
+ ```
165
+
166
+ Before you rename a model, plan how to carry its acceptances over. Acceptance
167
+ rows are read only through the model API.
168
+
76
169
  ## Render the host-owned form
77
170
 
78
171
  Keep the page, routes, authorization, document-link markup, and copy in your
@@ -106,10 +199,13 @@ host. Do not accept either value from browser parameters. Retries, double
106
199
  clicks, and concurrent submissions return the original acceptance evidence.
107
200
 
108
201
  A missing, malformed, wrong-agreement, or stale ID raises
109
- `Agreements::VersionNotCurrent`. Its `current_version` is ready to render:
202
+ `Agreements::VersionNotCurrent`. Its `current_version` is ready to render. It
203
+ is `nil` when no version exists for that key yet:
110
204
 
111
205
  ```ruby
112
206
  rescue Agreements::VersionNotCurrent => error
207
+ raise ActiveRecord::RecordNotFound unless error.current_version
208
+
113
209
  @version = error.current_version
114
210
  render :show, status: :unprocessable_content
115
211
  end
@@ -156,16 +252,68 @@ def require_organization_dpa
156
252
  end
157
253
  ```
158
254
 
159
- It redirects pending HTML/Turbo requests and remembers only GET or HEAD return
160
- locations. A blocked mutation is never replayed after acceptance. Consume the
161
- safe same-origin path after a successful acceptance:
255
+ When the agreement is pending:
256
+
257
+ - an HTML or Turbo Stream request is redirected to `location` with
258
+ `303 See Other`, and the method returns `:redirected`;
259
+ - any other request (JSON, CSV, PDF, API) gets an empty `403 Forbidden`, and
260
+ the method returns `:forbidden`.
261
+
262
+ The return value lets you skip work that should not run after a halted
263
+ request, such as Pundit's `verify_authorized`:
264
+
265
+ ```ruby
266
+ skip_authorization if require_agreement("organization_dpa", subject: Current.organization, location: path)
267
+ ```
268
+
269
+ Only GET and HEAD return locations are remembered. A blocked mutation is never
270
+ replayed after acceptance. Consume the safe same-origin path after a successful
271
+ acceptance:
162
272
 
163
273
  ```ruby
164
274
  redirect_to agreement_return_location || dashboard_path
165
275
  ```
166
276
 
167
- API and headless behavior remains host-owned: use `pending_version` and return
168
- the response contract your application supports.
277
+ If your API has its own error format, check before `require_agreement` runs:
278
+
279
+ ```ruby
280
+ def require_organization_dpa
281
+ if api_request?
282
+ if Agreements.pending_version("organization_dpa", subject: Current.organization)
283
+ render json: { error: "agreement_required" }, status: :forbidden
284
+ end
285
+ return
286
+ end
287
+
288
+ require_agreement("organization_dpa", subject: Current.organization, location: organization_dpa_path(Current.organization))
289
+ end
290
+ ```
291
+
292
+ When no version exists for a key, nothing is pending and nothing is blocked.
293
+ Insert the first version before you rely on enforcement.
294
+
295
+ ## Test your application
296
+
297
+ Once enforcement is on, most of your tests need subjects that have already
298
+ accepted. Include the test helper:
299
+
300
+ ```ruby
301
+ # test/test_helper.rb
302
+ require "agreements/test_helper"
303
+
304
+ class ActiveSupport::TestCase
305
+ include Agreements::TestHelper
306
+ end
307
+ ```
308
+
309
+ ```ruby
310
+ accept_agreement("user_terms", subject: user)
311
+ accept_agreement("organization_dpa", subject: organization, actor: owner, authority: "organization_owner")
312
+ ```
313
+
314
+ `accept_agreement` accepts the current version with its canonical statement and
315
+ the `en` locale. It raises when no current version exists, so insert versions
316
+ in your fixtures or setup first.
169
317
 
170
318
  ## Evidence model
171
319
 
@@ -175,6 +323,7 @@ the response contract your application supports.
175
323
  - human version label;
176
324
  - exact acceptance statement;
177
325
  - external document references and optional SHA-256 digests;
326
+ - optional effective date;
178
327
  - timestamps.
179
328
 
180
329
  `agreements_acceptances` contains:
@@ -204,12 +353,43 @@ controls.
204
353
 
205
354
  1. Publish immutable/versioned legal-document URLs and any verified digests.
206
355
  2. Add the finished bundle to the host's seed registry.
207
- 3. Add a host data migration that inserts the same immutable version.
208
- 4. Deploy. The new row becomes current and subjects missing it are prompted.
356
+ 3. Add a host data migration that inserts the same immutable version, with
357
+ `effective_at` if your terms promise notice.
358
+ 4. Deploy. The new row becomes current at once, or on its effective date, and
359
+ subjects missing it are prompted.
209
360
 
210
361
  Production deployments normally run migrations, not seeds. Changing a seed
211
362
  registry alone does not deploy a new version to an existing installation.
212
363
 
364
+ ## What the evidence proves
365
+
366
+ The gem records which version and which sentence a subject accepted, who did
367
+ it, under what authority, and when. That is the part your application can
368
+ prove later. Some questions it cannot answer for you:
369
+
370
+ - **The whole page counts.** Courts look at the entire screen, not only the
371
+ checkbox. In *Berman v. Freedom Financial Network* (9th Cir. 2022) the terms
372
+ did not bind the user: the notice was small grey text, the links did not
373
+ look like links, and the button said "Continue" without saying that it meant
374
+ acceptance. Your layout, contrast and button text decide this. The gem does
375
+ not see them.
376
+ - **It is not a qualified electronic signature.** Under eIDAS (Regulation (EU)
377
+ No 910/2014, Article 25), an electronic signature cannot be denied legal
378
+ effect only because it is electronic or not qualified. A checkbox is still
379
+ not a qualified electronic signature, and this gem does not create one.
380
+ - **Acknowledging a privacy notice is not consent.** Under the GDPR you inform
381
+ people (Article 13). Consent (Articles 6 and 7) is a separate, specific and
382
+ withdrawable choice. Keep the two apart in your copy and your records.
383
+ - **The text of your terms still matters.** In the EU, an unfair term in a
384
+ consumer contract does not bind the consumer (Directive 93/13/EEC), however
385
+ clearly it was accepted.
386
+ - **Identity and authority are yours to check.** The gem stores the actor and
387
+ the authority you pass. It does not verify who the person is or whether they
388
+ may bind an organization.
389
+
390
+ Your counsel decides what your agreements need. The gem keeps the record of
391
+ what was accepted.
392
+
213
393
  ## Not this gem
214
394
 
215
395
  - Legal-document hosting, editing, rendering, uploads, or a CMS.
@@ -223,7 +403,9 @@ registry alone does not deploy a new version to an existing installation.
223
403
  layouts, routes, or copy.
224
404
 
225
405
  Those are real product categories. They are not prerequisites for proving an
226
- ordinary Terms or organization-DPA acceptance.
406
+ ordinary Terms or organization-DPA acceptance. If you need consent withdrawal,
407
+ signed receipts, request evidence or a form builder, look at
408
+ [clickwrap](https://github.com/rameerez/clickwrap).
227
409
 
228
410
  ## Later, only after a concrete trigger
229
411
 
@@ -5,6 +5,7 @@ require "uri"
5
5
  module Agreements
6
6
  class Version < ApplicationRecord
7
7
  SHA256_PATTERN = /\A[0-9a-f]{64}\z/i
8
+ KEY_PATTERN = /\A[a-z0-9_]+\z/
8
9
 
9
10
  has_many :acceptances,
10
11
  class_name: "Agreements::Acceptance",
@@ -12,12 +13,33 @@ module Agreements
12
13
  inverse_of: :agreement_version,
13
14
  dependent: :restrict_with_exception
14
15
 
15
- validates :agreement_key, presence: true, format: { with: /\A[a-z0-9_]+\z/ }, uniqueness: { scope: :version }
16
+ validates :agreement_key, presence: true, format: { with: KEY_PATTERN }, uniqueness: { scope: :version }
16
17
  validates :version, :acceptance_statement, presence: true
17
18
  validate :documents_are_external_references
18
19
 
19
- def self.current_for(agreement_key)
20
- where(agreement_key: agreement_key).order(created_at: :desc, id: :desc).first
20
+ # Filtering by time in Ruby keeps the SQL identical, so the query cache
21
+ # serves repeated checks within a request. Each key has only a few versions.
22
+ def self.current_for(agreement_key, now: Time.current)
23
+ versions = where(agreement_key: agreement_key).to_a
24
+ versions.select { |version| version.in_effect?(now) }.max_by(&:precedence)
25
+ end
26
+
27
+ def self.upcoming_for(agreement_key, now: Time.current)
28
+ scheduled = where(agreement_key: agreement_key).to_a.reject { |version| version.in_effect?(now) }
29
+ next_from = scheduled.map(&:effective_from).min
30
+ scheduled.select { |version| version.effective_from == next_from }.max_by(&:precedence)
31
+ end
32
+
33
+ def precedence
34
+ [effective_from, created_at, id]
35
+ end
36
+
37
+ def effective_from
38
+ effective_at || created_at
39
+ end
40
+
41
+ def in_effect?(now = Time.current)
42
+ effective_from <= now
21
43
  end
22
44
 
23
45
  def accepted_by?(subject)
@@ -51,13 +73,18 @@ module Agreements
51
73
  end
52
74
 
53
75
  def allowed_document_attributes?(attributes)
54
- attributes.keys.all? { |key| %w[title url sha256].include?(key) }
76
+ attributes.keys.all? { |key| %w[key title url sha256].include?(key) }
55
77
  end
56
78
 
57
79
  def complete_document_attributes?(attributes)
58
80
  attributes["title"].present? &&
59
81
  external_https_url?(attributes["url"]) &&
60
- valid_sha256?(attributes["sha256"])
82
+ valid_sha256?(attributes["sha256"]) &&
83
+ valid_document_key?(attributes["key"])
84
+ end
85
+
86
+ def valid_document_key?(key)
87
+ key.nil? || key.to_s.match?(KEY_PATTERN)
61
88
  end
62
89
 
63
90
  def external_https_url?(url)
@@ -9,9 +9,13 @@ module Agreements
9
9
  private
10
10
 
11
11
  def require_agreement(agreement_key, subject:, location:)
12
- return unless agreement_enforcement_request?
13
12
  return unless Agreements.pending_version(agreement_key, subject: subject)
14
13
 
14
+ unless agreement_enforcement_request?
15
+ head :forbidden
16
+ return :forbidden
17
+ end
18
+
15
19
  remember_agreement_return_location
16
20
  redirect_to location, status: :see_other
17
21
  :redirected
@@ -30,8 +34,7 @@ module Agreements
30
34
  end
31
35
 
32
36
  def agreement_return_location
33
- path = session.delete(:return_to_after_agreement)
34
- path if path&.start_with?("/") && !path.start_with?("//")
37
+ url_from(session.delete(:return_to_after_agreement))
35
38
  end
36
39
  end
37
40
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "global_id/railtie"
4
+
3
5
  module Agreements
4
6
  class Engine < ::Rails::Engine
5
7
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agreements
4
+ module TestHelper
5
+ def accept_agreement(agreement_key, subject:, actor: subject, authority: "self", locale: "en")
6
+ version = Agreements.current_version(agreement_key)
7
+ raise ArgumentError, "no current #{agreement_key} version; insert one in your test setup" unless version
8
+
9
+ Agreements.accept!(
10
+ agreement_key,
11
+ version_id: version.id,
12
+ subject: subject,
13
+ actor: actor,
14
+ authority: authority,
15
+ acceptance_statement: version.acceptance_statement,
16
+ locale: locale
17
+ )
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agreements
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/agreements.rb CHANGED
@@ -17,6 +17,10 @@ module Agreements
17
17
  Version.current_for(agreement_key)
18
18
  end
19
19
 
20
+ def upcoming_version(agreement_key)
21
+ Version.upcoming_for(agreement_key)
22
+ end
23
+
20
24
  def pending_version(agreement_key, subject:)
21
25
  current_version(agreement_key).then do |version|
22
26
  version unless version.nil? || version.accepted_by?(subject)
@@ -7,6 +7,7 @@ class CreateAgreementsTables < ActiveRecord::Migration<%= migration_version %>
7
7
  t.string :version, null: false
8
8
  t.text :acceptance_statement, null: false
9
9
  t.json :documents, null: false, default: []
10
+ t.datetime :effective_at
10
11
  t.timestamps
11
12
 
12
13
  t.index %i[agreement_key version], unique: true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agreements
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -10,7 +10,61 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rails
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: activerecord
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '7.1'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '7.1'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
52
+ - !ruby/object:Gem::Dependency
53
+ name: globalid
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '1.0'
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '1.0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: railties
14
68
  requirement: !ruby/object:Gem::Requirement
15
69
  requirements:
16
70
  - - ">="
@@ -54,6 +108,7 @@ files:
54
108
  - lib/agreements/errors.rb
55
109
  - lib/agreements/identity.rb
56
110
  - lib/agreements/recorder.rb
111
+ - lib/agreements/test_helper.rb
57
112
  - lib/agreements/version.rb
58
113
  - lib/generators/agreements/install/install_generator.rb
59
114
  - lib/generators/agreements/install/templates/create_agreements_tables.rb.tt
@@ -81,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
136
  - !ruby/object:Gem::Version
82
137
  version: '0'
83
138
  requirements: []
84
- rubygems_version: 4.0.16
139
+ rubygems_version: 4.0.20
85
140
  specification_version: 4
86
141
  summary: Auditable acceptance of externally hosted legal agreements for Rails.
87
142
  test_files: []