rails_api_keys 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f6a89bf64215fd1c3fcad121b300e716eeff78787be8f438e63dca2c399b6bf7
4
+ data.tar.gz: 2d4ec14c89eb7e8675d0036ff18d879532b74b93ff1311320b0d5f5a75531a50
5
+ SHA512:
6
+ metadata.gz: 9552c48d8976f5b1df454460273d9616a9203d2bb9a6987b643981b99d0ac3b93b2cd21af5c948afdaf4b414a48ec091cc2eb8f3699b77240a371a42f16c02b7
7
+ data.tar.gz: f245f531718d6e288d1d108de0aee915f0f18a78dcbd743c0f44a1e7a86511446856719ef15b38370f7545c5e30104acbb7eab45fa65b4c4048d479f533f7ecf
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-08-10
11
+
12
+ ### Added
13
+
14
+ - `RailsApiKeys::ApiKey` — create (raw token once), SHA-256 digest storage, soft revoke, `read` / `read_write` permissions
15
+ - `RailsApiKeys::Authentication` — Bearer token auth helpers for `ActionController::API`
16
+ - `RailsApiKeys.configure` — optional `owner_class`, `token_prefix`, and `owner_active`
17
+ - Install generator (`rails_api_keys:install`) for migration and initializer
18
+
19
+ [Unreleased]: https://github.com/rubyroidlabs/rails_api_keys/compare/v0.1.0...HEAD
20
+ [0.1.0]: https://github.com/rubyroidlabs/rails_api_keys/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rubyroid Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # rails_api_keys
2
+
3
+ [![CI](https://github.com/rubyroidlabs/rails_api_keys/actions/workflows/ci.yml/badge.svg)](https://github.com/rubyroidlabs/rails_api_keys/actions/workflows/ci.yml)
4
+
5
+ Reusable Rails engine for personal API keys: create (reveal once), hash at rest, revoke, and authenticate via `Authorization: Bearer <token>`.
6
+
7
+ Host apps own UI, routes, and domain APIs. This gem stays thin on purpose.
8
+
9
+ ## What this gem includes
10
+
11
+ - `RailsApiKeys::ApiKey` — issue, authenticate, revoke
12
+ - `RailsApiKeys::Authentication` — controller concern for Bearer tokens
13
+ - Install generator (migration + initializer)
14
+
15
+ No mailers, jobs, views, assets, or mounted domain routes.
16
+
17
+ ## Installation
18
+
19
+ Add the gem and install:
20
+
21
+ ```ruby
22
+ # Gemfile
23
+ gem "rails_api_keys"
24
+ ```
25
+
26
+ ```bash
27
+ bundle install
28
+ bin/rails generate rails_api_keys:install
29
+ bin/rails db:migrate
30
+ ```
31
+
32
+ Until the gem is published on RubyGems, you can use git or a local path:
33
+
34
+ ```ruby
35
+ gem "rails_api_keys", git: "https://github.com/rubyroidlabs/rails_api_keys.git"
36
+ # or: gem "rails_api_keys", path: "../rails_api_keys"
37
+ ```
38
+
39
+ ## Host setup
40
+
41
+ On the owner model (defaults to `User`):
42
+
43
+ ```ruby
44
+ class User < ApplicationRecord
45
+ has_many :api_keys, as: :owner, class_name: "RailsApiKeys::ApiKey", dependent: :destroy
46
+ end
47
+ ```
48
+
49
+ Build your own controllers/UI to create keys (show the raw token once), list them, and revoke.
50
+
51
+ ## Configuration
52
+
53
+ Defaults suit a typical Devise `User` host:
54
+
55
+ | Option | Default | Purpose |
56
+ | --- | --- | --- |
57
+ | `owner_class` | `"User"` | Expected owner class name |
58
+ | `token_prefix` | `"#{AppName.downcase}_ak_"` | Prefix on generated raw tokens |
59
+ | `owner_active` | `active_for_authentication?` when present | Reject keys whose owner is inactive |
60
+
61
+ Override only what you need:
62
+
63
+ ```ruby
64
+ # config/initializers/rails_api_keys.rb
65
+ RailsApiKeys.configure do |config|
66
+ # config.token_prefix = "myapp_ak_"
67
+ # config.owner_class = "Admin"
68
+ # config.owner_active = ->(owner) { owner.active? }
69
+ end
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ ```ruby
75
+ key, raw = RailsApiKeys::ApiKey.generate_for!(
76
+ owner: current_user,
77
+ name: "Zapier",
78
+ permission: :read
79
+ )
80
+ # Show `raw` once — it cannot be recovered later.
81
+
82
+ RailsApiKeys::ApiKey.authenticate(raw) # => key or nil
83
+ key.revoke!
84
+ ```
85
+
86
+ ```ruby
87
+ class Api::V1::BaseController < ActionController::API
88
+ include RailsApiKeys::Authentication
89
+
90
+ before_action :authenticate_api_key!
91
+ before_action -> { require_api_permission!(:read) }
92
+
93
+ # current_api_key / current_api_owner are available after authenticate
94
+ end
95
+ ```
96
+
97
+ Clients send:
98
+
99
+ ```http
100
+ Authorization: Bearer <raw_token>
101
+ ```
102
+
103
+ ## Permissions
104
+
105
+ | Permission | `allows_read?` | `allows_write?` |
106
+ | --- | --- | --- |
107
+ | `read` | yes | no |
108
+ | `read_write` | yes | yes |
109
+
110
+ Permissions are immutable after create. Revoke with `revoke!` (sets `revoked_at`).
111
+
112
+ ## Security notes
113
+
114
+ - Raw tokens are returned only from `generate_for!` and never stored
115
+ - Digests use SHA-256 (`token_digest`); UI can show `token_display_prefix`
116
+ - Soft revoke via `revoked_at`; authentication ignores revoked keys
117
+
118
+ ## Development
119
+
120
+ ```bash
121
+ bundle install
122
+ bundle exec rspec
123
+ bin/rubocop
124
+ ```
125
+
126
+ See [CHANGELOG.md](./CHANGELOG.md) for release notes. Agent-oriented notes live in [`AGENTS.md`](./AGENTS.md).
127
+
128
+ ## License
129
+
130
+ This project is licensed under the [MIT License](./LICENSE).
131
+
132
+ ---
133
+
134
+ <p align="center">
135
+ <a href="https://rubyroidlabs.com">
136
+ <img src="./docs/logo.svg" alt="Rubyroid Labs" width="100%" />
137
+ </a>
138
+ </p>
139
+
140
+ **[Rubyroid Labs](https://rubyroidlabs.com)** — full-cycle software development company for businesses delivering scalable web and mobile apps, dedicated developers, and full-cycle teams in Ruby on Rails, React Native, and UX/UI. 98% on-time delivery.
141
+
142
+ - Website: [rubyroidlabs.com](https://rubyroidlabs.com)
143
+ - Email: [hi@rubyroidlabs.com](mailto:hi@rubyroidlabs.com)
144
+ - References: [Clutch](https://clutch.co/profile/rubyroid-labs)
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "securerandom"
5
+
6
+ module RailsApiKeys
7
+ # Persists personal API keys for a polymorphic owner.
8
+ #
9
+ # Raw tokens are returned only from {.generate_for!} and are never stored.
10
+ # Authentication matches against a SHA-256 digest and refuses revoked or
11
+ # inactive-owner keys.
12
+ class ApiKey < ApplicationRecord
13
+ self.table_name = "rails_api_keys_api_keys"
14
+
15
+ DISPLAY_PREFIX_LENGTH = 12
16
+ TOKEN_BYTES = 32
17
+
18
+ belongs_to :owner, polymorphic: true
19
+
20
+ enum :permission, { read: "read", read_write: "read_write" }, validate: true
21
+
22
+ validates :name, :token_digest, :token_display_prefix, :permission, presence: true
23
+ validates :token_digest, uniqueness: true
24
+ validate :owner_matches_configured_class
25
+ validate :permission_immutable, on: :update
26
+
27
+ scope :active, -> { where(revoked_at: nil) }
28
+
29
+ class << self
30
+ # Creates a key and returns +[record, raw_token]+. Show +raw_token+ once.
31
+ def generate_for!(owner:, name:, permission:)
32
+ raw_token = build_raw_token
33
+ record = create!(
34
+ owner: owner,
35
+ name: name,
36
+ permission: permission,
37
+ token_digest: digest(raw_token),
38
+ token_display_prefix: display_prefix_for(raw_token)
39
+ )
40
+ [ record, raw_token ]
41
+ end
42
+
43
+ # Returns an active key for +raw_token+, or +nil+. Touches +last_used_at+.
44
+ def authenticate(raw_token)
45
+ return if raw_token.blank?
46
+
47
+ key = active.find_by(token_digest: digest(raw_token))
48
+ return unless key
49
+ return unless RailsApiKeys.configuration.owner_active?(key.owner)
50
+
51
+ key.touch_last_used!
52
+ key
53
+ end
54
+
55
+ def digest(raw_token)
56
+ Digest::SHA256.hexdigest(raw_token.to_s)
57
+ end
58
+
59
+ def build_raw_token
60
+ "#{RailsApiKeys.configuration.resolved_token_prefix}#{SecureRandom.urlsafe_base64(TOKEN_BYTES)}"
61
+ end
62
+
63
+ def display_prefix_for(raw_token)
64
+ raw_token.to_s[0, DISPLAY_PREFIX_LENGTH]
65
+ end
66
+ end
67
+
68
+ # Soft-revokes the key so {.authenticate} no longer accepts it.
69
+ def revoke!
70
+ update!(revoked_at: Time.current)
71
+ end
72
+
73
+ def revoked?
74
+ revoked_at.present?
75
+ end
76
+
77
+ def active?
78
+ !revoked?
79
+ end
80
+
81
+ def allows_read?
82
+ read? || read_write?
83
+ end
84
+
85
+ def allows_write?
86
+ read_write?
87
+ end
88
+
89
+ def touch_last_used!
90
+ update_column(:last_used_at, Time.current)
91
+ end
92
+
93
+ private
94
+
95
+ def owner_matches_configured_class
96
+ return if owner.blank?
97
+
98
+ expected = RailsApiKeys.configuration.owner_class_name
99
+ return if owner.class.name == expected
100
+
101
+ errors.add(:owner, "must be a #{expected}")
102
+ end
103
+
104
+ def permission_immutable
105
+ return unless permission_changed? && persisted?
106
+
107
+ errors.add(:permission, "cannot be changed after creation")
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,5 @@
1
+ module RailsApiKeys
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ RailsApiKeys::Engine.routes.draw do
4
+ # No engine-mounted routes; host owns key management UI and API endpoints.
5
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRailsApiKeysApiKeys < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :rails_api_keys_api_keys do |t|
6
+ t.string :name, null: false
7
+ t.string :token_digest, null: false
8
+ t.string :token_display_prefix, null: false
9
+ t.string :permission, null: false, default: "read"
10
+ t.references :owner, polymorphic: true, null: false, index: true
11
+ t.datetime :revoked_at
12
+ t.datetime :last_used_at
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :rails_api_keys_api_keys, :token_digest, unique: true
17
+ add_index :rails_api_keys_api_keys, :revoked_at
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+
6
+ module RailsApiKeys
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Install RailsApiKeys migrations and initializer"
14
+
15
+ def self.next_migration_number(dirname)
16
+ next_number = Time.now.utc.strftime("%Y%m%d%H%M%S")
17
+ if Dir.exist?(dirname)
18
+ while Dir.children(dirname).any? { |f| f.start_with?(next_number) }
19
+ next_number = (next_number.to_i + 1).to_s
20
+ end
21
+ end
22
+ next_number
23
+ end
24
+
25
+ def copy_migration
26
+ migration_template(
27
+ "create_rails_api_keys_api_keys.rb.tt",
28
+ "db/migrate/create_rails_api_keys_api_keys.rb"
29
+ )
30
+ end
31
+
32
+ def copy_initializer
33
+ template "initializer.rb.tt", "config/initializers/rails_api_keys.rb"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRailsApiKeysApiKeys < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def change
5
+ create_table :rails_api_keys_api_keys do |t|
6
+ t.string :name, null: false
7
+ t.string :token_digest, null: false
8
+ t.string :token_display_prefix, null: false
9
+ t.string :permission, null: false, default: "read"
10
+ t.references :owner, polymorphic: true, null: false, index: true
11
+ t.datetime :revoked_at
12
+ t.datetime :last_used_at
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :rails_api_keys_api_keys, :token_digest, unique: true
17
+ add_index :rails_api_keys_api_keys, :revoked_at
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Defaults: owner_class "User", token_prefix "<AppName>_ak_",
4
+ # owner_active via active_for_authentication? when available.
5
+ # Uncomment to override:
6
+ #
7
+ # RailsApiKeys.configure do |config|
8
+ # config.token_prefix = "myapp_ak_"
9
+ # end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module RailsApiKeys
6
+ # Controller concern for +ActionController::API+.
7
+ #
8
+ # Include and call {#authenticate_api_key!} / {#require_api_permission!} from
9
+ # +before_action+. After a successful authenticate, +current_api_key+ and
10
+ # +current_api_owner+ are set.
11
+ #
12
+ # Expects +Authorization: Bearer <raw_token>+.
13
+ module Authentication
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ attr_reader :current_api_key, :current_api_owner
18
+ end
19
+
20
+ private
21
+
22
+ # Authenticates the Bearer token. Renders 401 and returns +false+ on failure.
23
+ def authenticate_api_key!
24
+ token = bearer_token
25
+ key = RailsApiKeys::ApiKey.authenticate(token)
26
+
27
+ unless key
28
+ render_api_unauthorized
29
+ return false
30
+ end
31
+
32
+ @current_api_key = key
33
+ @current_api_owner = key.owner
34
+ true
35
+ end
36
+
37
+ # Requires +:read+ or +:write+. +read_write+ keys satisfy both. Renders 403 on failure.
38
+ def require_api_permission!(level)
39
+ allowed =
40
+ case level.to_sym
41
+ when :read then current_api_key&.allows_read?
42
+ when :write then current_api_key&.allows_write?
43
+ else false
44
+ end
45
+
46
+ return true if allowed
47
+
48
+ render json: { error: "Forbidden" }, status: :forbidden
49
+ false
50
+ end
51
+
52
+ def bearer_token
53
+ header = request.authorization.to_s
54
+ return unless header.match?(/\ABearer\s+/i)
55
+
56
+ header.split(/\s+/, 2).last.presence
57
+ end
58
+
59
+ def render_api_unauthorized
60
+ render json: { error: "Unauthorized" }, status: :unauthorized
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiKeys
4
+ # Optional host overrides. Set via {RailsApiKeys.configure}.
5
+ #
6
+ # Defaults: +owner_class+ is +"User"+, +token_prefix+ derives from the app
7
+ # name, and +owner_active+ uses Devise-style +active_for_authentication?+
8
+ # when available.
9
+ class Configuration
10
+ attr_accessor :owner_class, :token_prefix, :owner_active
11
+
12
+ def initialize
13
+ @owner_class = "User"
14
+ @token_prefix = nil
15
+ @owner_active = ->(owner) {
16
+ if owner.respond_to?(:active_for_authentication?)
17
+ owner.active_for_authentication?
18
+ else
19
+ true
20
+ end
21
+ }
22
+ end
23
+
24
+ def resolved_token_prefix
25
+ return token_prefix.to_s if token_prefix.present?
26
+
27
+ "#{Rails.application.class.module_parent_name.downcase}_ak_"
28
+ end
29
+
30
+ def owner_class_name
31
+ owner_class.to_s
32
+ end
33
+
34
+ def owner_active?(owner)
35
+ owner_active.call(owner)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiKeys
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace RailsApiKeys
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiKeys
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_api_keys/version"
4
+ require "rails_api_keys/configuration"
5
+ require "rails_api_keys/authentication"
6
+ require "rails_api_keys/engine"
7
+
8
+ # Thin Rails engine for personal API key authentication.
9
+ #
10
+ # @see file:README.md
11
+ module RailsApiKeys
12
+ class << self
13
+ # @return [RailsApiKeys::Configuration]
14
+ def configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+
18
+ # Yields {configuration} for host overrides in an initializer.
19
+ def configure
20
+ yield configuration
21
+ end
22
+
23
+ # Resets configuration to defaults (primarily for tests).
24
+ def reset_configuration!
25
+ @configuration = Configuration.new
26
+ end
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_api_keys
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pavel Pershko
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ description: |
27
+ Thin Rails engine for personal API keys: issue a raw token once, store a
28
+ SHA-256 digest, soft-revoke, and authenticate ActionController::API
29
+ requests with Authorization: Bearer. Host apps own UI, routes, and domain APIs.
30
+ email:
31
+ - pavel.pershko@rubyroidlabs.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files:
35
+ - CHANGELOG.md
36
+ - LICENSE
37
+ - README.md
38
+ files:
39
+ - CHANGELOG.md
40
+ - LICENSE
41
+ - README.md
42
+ - app/models/rails_api_keys/api_key.rb
43
+ - app/models/rails_api_keys/application_record.rb
44
+ - config/routes.rb
45
+ - db/migrate/20260810120000_create_rails_api_keys_api_keys.rb
46
+ - lib/generators/rails_api_keys/install/install_generator.rb
47
+ - lib/generators/rails_api_keys/install/templates/create_rails_api_keys_api_keys.rb.tt
48
+ - lib/generators/rails_api_keys/install/templates/initializer.rb.tt
49
+ - lib/rails_api_keys.rb
50
+ - lib/rails_api_keys/authentication.rb
51
+ - lib/rails_api_keys/configuration.rb
52
+ - lib/rails_api_keys/engine.rb
53
+ - lib/rails_api_keys/version.rb
54
+ homepage: https://github.com/rubyroidlabs/rails_api_keys
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://github.com/rubyroidlabs/rails_api_keys
59
+ source_code_uri: https://github.com/rubyroidlabs/rails_api_keys/tree/v0.1.0
60
+ changelog_uri: https://github.com/rubyroidlabs/rails_api_keys/blob/v0.1.0/CHANGELOG.md
61
+ bug_tracker_uri: https://github.com/rubyroidlabs/rails_api_keys/issues
62
+ documentation_uri: https://www.rubydoc.info/gems/rails_api_keys/0.1.0
63
+ rubygems_mfa_required: 'true'
64
+ allowed_push_host: https://rubygems.org
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.2.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 4.0.18
80
+ specification_version: 4
81
+ summary: Reusable Rails engine for personal API key authentication.
82
+ test_files: []