bard-api 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 +7 -0
- data/.rspec +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/Rakefile +8 -0
- data/config.ru +5 -0
- data/lib/bard/api/app.rb +88 -0
- data/lib/bard/api/auth.rb +56 -0
- data/lib/bard/api/backup.rb +95 -0
- data/lib/bard/api/version.rb +7 -0
- data/lib/bard/api.rb +12 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 88e72bcfdbda52d9fe76f647c7f29b18f6610c755c416728dfa4d5333f8b45e5
|
|
4
|
+
data.tar.gz: 55a480c5e5e72e4041c3bf9a1eb973104af8b40da1d31a1c9eb289ab5eb97ac8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c64aaf163b756bc6f02286ecccca3aed8fc1835b15fc7b991fb9eed9662e187bdfc74802cf624c578759894de0967c83a69f0a27d7d739dffa9f351808c148a1
|
|
7
|
+
data.tar.gz: ab6a6c87a4ae2a692cdb3039fb087749b882eeb7cb1c0599c9b6eba9a221c3243cab4b3c78a118438c9a03ca8a951a5ff4cd8a3f3e11d1f7795a3c67e1c37414
|
data/.rspec
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Micah Geisel
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Bard::Api
|
|
2
|
+
|
|
3
|
+
REST API for BARD-managed Rails projects. This gem provides a lightweight Rack application that mounts in Rails projects to expose management endpoints for BARD Tracker.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The bard-api gem enables BARD Tracker to manage Rails applications through a REST API. It provides:
|
|
8
|
+
|
|
9
|
+
- **Database backups**: Trigger and monitor database backups using Backhoe
|
|
10
|
+
- **Health monitoring**: Check application status
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Mounting in Rails
|
|
15
|
+
|
|
16
|
+
Add to your `config/routes.rb`:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
mount Bard::Api::App.new => "/bard-api"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This makes the API available at `/bard-api/*` endpoints.
|
|
23
|
+
|
|
24
|
+
### Endpoints
|
|
25
|
+
|
|
26
|
+
#### GET /bard-api/health
|
|
27
|
+
|
|
28
|
+
Health check endpoint (no authentication required).
|
|
29
|
+
|
|
30
|
+
**Response:**
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"status": "ok"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### POST /bard-api/backups
|
|
38
|
+
|
|
39
|
+
Trigger a backup (requires JWT authentication).
|
|
40
|
+
|
|
41
|
+
**Headers:**
|
|
42
|
+
```
|
|
43
|
+
Authorization: Bearer <jwt-token>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Request:**
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"urls": [
|
|
50
|
+
"https://s3.amazonaws.com/presigned-url..."
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Response (200 OK):**
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"timestamp": "2025-12-06T10:30:00Z",
|
|
59
|
+
"size": 123456789,
|
|
60
|
+
"destinations": [
|
|
61
|
+
{
|
|
62
|
+
"name": "bard",
|
|
63
|
+
"type": "bard",
|
|
64
|
+
"status": "success"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### GET /bard-api/backups/latest
|
|
71
|
+
|
|
72
|
+
Get status of most recent backup (requires JWT authentication).
|
|
73
|
+
|
|
74
|
+
**Headers:**
|
|
75
|
+
```
|
|
76
|
+
Authorization: Bearer <jwt-token>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Response (200 OK):**
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"timestamp": "2025-12-06T10:30:00Z",
|
|
83
|
+
"size": 123456789,
|
|
84
|
+
"destinations": [
|
|
85
|
+
{
|
|
86
|
+
"name": "primary",
|
|
87
|
+
"type": "s3",
|
|
88
|
+
"status": "success"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Authentication
|
|
95
|
+
|
|
96
|
+
The API uses JWT with asymmetric RSA keys for authentication. The public key is embedded in the gem, and only BARD Tracker with the private key can create valid tokens.
|
|
97
|
+
|
|
98
|
+
Tokens expire after 5 minutes and must include:
|
|
99
|
+
- `urls`: Array of presigned S3 URLs for backup destinations
|
|
100
|
+
- `exp`: Expiration timestamp
|
|
101
|
+
- `iat`: Issued at timestamp
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/config.ru
ADDED
data/lib/bard/api/app.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "auth"
|
|
6
|
+
require_relative "backup"
|
|
7
|
+
|
|
8
|
+
module Bard
|
|
9
|
+
module Api
|
|
10
|
+
class App
|
|
11
|
+
def call(env)
|
|
12
|
+
request = Rack::Request.new(env)
|
|
13
|
+
method = request.request_method
|
|
14
|
+
path = request.path_info
|
|
15
|
+
|
|
16
|
+
case [method, path]
|
|
17
|
+
when ["GET", "/health"]
|
|
18
|
+
health(request)
|
|
19
|
+
when ["POST", "/backups"]
|
|
20
|
+
create_backup(request)
|
|
21
|
+
when ["GET", "/backups/latest"]
|
|
22
|
+
latest_backup(request)
|
|
23
|
+
else
|
|
24
|
+
not_found
|
|
25
|
+
end
|
|
26
|
+
rescue => e
|
|
27
|
+
internal_error(e)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def health(request)
|
|
33
|
+
json_response(200, { status: "ok" })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create_backup(request)
|
|
37
|
+
with_auth(request) do |payload|
|
|
38
|
+
# Extract URLs from the JWT payload
|
|
39
|
+
urls = payload["urls"]
|
|
40
|
+
raise "Missing 'urls' in token payload" if urls.nil? || urls.empty?
|
|
41
|
+
|
|
42
|
+
# Perform the backup
|
|
43
|
+
backup = Backup.new
|
|
44
|
+
result = backup.perform(urls)
|
|
45
|
+
|
|
46
|
+
json_response(200, result)
|
|
47
|
+
end
|
|
48
|
+
rescue => e
|
|
49
|
+
json_response(500, { error: "Backup failed: #{e.message}" })
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def latest_backup(request)
|
|
53
|
+
with_auth(request) do
|
|
54
|
+
# Get the latest backup status
|
|
55
|
+
backup = Backup.new
|
|
56
|
+
result = backup.latest
|
|
57
|
+
|
|
58
|
+
if result
|
|
59
|
+
json_response(200, result)
|
|
60
|
+
else
|
|
61
|
+
json_response(404, { error: "No backups found" })
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
rescue => e
|
|
65
|
+
json_response(500, { error: e.message })
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def with_auth(request)
|
|
69
|
+
payload = Auth.verify!(request.env["HTTP_AUTHORIZATION"])
|
|
70
|
+
yield payload
|
|
71
|
+
rescue Auth::AuthenticationError => e
|
|
72
|
+
json_response(401, { error: "Unauthorized: #{e.message}" })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def json_response(status, body)
|
|
76
|
+
Rack::Response.new(body.to_json, status, { "Content-Type" => "application/json" }).finish
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def not_found
|
|
80
|
+
json_response(404, { error: "Not found" })
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def internal_error(e)
|
|
84
|
+
json_response(500, { error: "Internal server error: #{e.message}" })
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jwt"
|
|
4
|
+
|
|
5
|
+
module Bard
|
|
6
|
+
module Api
|
|
7
|
+
class Auth
|
|
8
|
+
# BARD Tracker's public RSA key for JWT verification
|
|
9
|
+
# This public key can be safely included in the open-source gem
|
|
10
|
+
# Only BARD Tracker with the private key can create valid tokens
|
|
11
|
+
BARD_PUBLIC_KEY = OpenSSL::PKey::RSA.new(<<~KEY)
|
|
12
|
+
-----BEGIN PUBLIC KEY-----
|
|
13
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyWnrycx4wOR8Hm73F60L
|
|
14
|
+
x0iadR9+r40BRvUPGApg21fxMrNa263rH0nM+W8BX44YpusA1yEbxchbh6lvz7J7
|
|
15
|
+
msjwBgqmpUaSZrSDapoGm7D0bTmXPun84BvFbw0GGOP3K0FYfl859ylaqKw1LyxW
|
|
16
|
+
+b6OK7ccOAM6LmAcILTL9ox4e87SLctXc/Nu8I2Fcj4U83q8chgbtu2JsrqfP8sH
|
|
17
|
+
do0B/dOZRP3Ciwu2tPkwggBVKxGs4dIrXQzjCs7EhKYGGwKa4nyI2/IONebq0w9Q
|
|
18
|
+
QRkn7oivSUNXW3Y+iznoapwgo5c5IO82OrfaQ2tGMvhqtzDa3KNY96ebVCX8HHV/
|
|
19
|
+
gQIDAQAB
|
|
20
|
+
-----END PUBLIC KEY-----
|
|
21
|
+
KEY
|
|
22
|
+
|
|
23
|
+
class AuthenticationError < StandardError; end
|
|
24
|
+
|
|
25
|
+
def initialize(token)
|
|
26
|
+
@token = token
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def verify!
|
|
30
|
+
raise AuthenticationError, "Missing authorization token" if @token.nil? || @token.empty?
|
|
31
|
+
|
|
32
|
+
# Remove 'Bearer ' prefix if present
|
|
33
|
+
token = @token.start_with?("Bearer ") ? @token[7..] : @token
|
|
34
|
+
|
|
35
|
+
# Decode and verify the JWT
|
|
36
|
+
payload = JWT.decode(
|
|
37
|
+
token,
|
|
38
|
+
BARD_PUBLIC_KEY,
|
|
39
|
+
true,
|
|
40
|
+
algorithm: "RS256"
|
|
41
|
+
).first
|
|
42
|
+
|
|
43
|
+
# Return the payload for use by the caller
|
|
44
|
+
payload
|
|
45
|
+
rescue JWT::ExpiredSignature
|
|
46
|
+
raise AuthenticationError, "Token has expired"
|
|
47
|
+
rescue JWT::DecodeError => e
|
|
48
|
+
raise AuthenticationError, "Invalid token: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.verify!(token)
|
|
52
|
+
new(token).verify!
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "backhoe"
|
|
4
|
+
require "http"
|
|
5
|
+
require "time"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
module Bard
|
|
9
|
+
module Api
|
|
10
|
+
class Backup
|
|
11
|
+
class BackupError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Perform a backup to the specified URLs
|
|
14
|
+
def perform(urls)
|
|
15
|
+
raise BackupError, "No URLs provided" if urls.nil? || urls.empty?
|
|
16
|
+
|
|
17
|
+
timestamp = Time.now.utc
|
|
18
|
+
destinations = []
|
|
19
|
+
errors = []
|
|
20
|
+
|
|
21
|
+
# Create temp file with timestamp
|
|
22
|
+
filename = "#{timestamp.iso8601}.sql.gz"
|
|
23
|
+
temp_path = "/tmp/#{filename}"
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
# Dump database to temp file using Backhoe
|
|
27
|
+
Backhoe.dump(temp_path)
|
|
28
|
+
|
|
29
|
+
# Get file size
|
|
30
|
+
file_size = File.size(temp_path)
|
|
31
|
+
|
|
32
|
+
# Upload to all URLs in parallel
|
|
33
|
+
threads = urls.map do |url|
|
|
34
|
+
Thread.new do
|
|
35
|
+
begin
|
|
36
|
+
upload_to_url(url, temp_path)
|
|
37
|
+
{
|
|
38
|
+
name: "bard",
|
|
39
|
+
type: "bard",
|
|
40
|
+
status: "success"
|
|
41
|
+
}
|
|
42
|
+
rescue => e
|
|
43
|
+
errors << e
|
|
44
|
+
{
|
|
45
|
+
name: "bard",
|
|
46
|
+
type: "bard",
|
|
47
|
+
status: "failed",
|
|
48
|
+
error: e.message
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Wait for all uploads to complete
|
|
55
|
+
destinations = threads.map(&:value)
|
|
56
|
+
ensure
|
|
57
|
+
# Clean up temp file
|
|
58
|
+
FileUtils.rm_f(temp_path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Store backup metadata
|
|
62
|
+
@last_backup = {
|
|
63
|
+
timestamp: timestamp.iso8601,
|
|
64
|
+
size: file_size,
|
|
65
|
+
destinations: destinations
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Raise error if any destination failed
|
|
69
|
+
unless errors.empty?
|
|
70
|
+
raise BackupError, "Some destinations failed: #{errors.map(&:message).join(", ")}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@last_backup
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get the latest backup status
|
|
77
|
+
def latest
|
|
78
|
+
# TODO: Retrieve from database instead of instance variable
|
|
79
|
+
@last_backup
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def upload_to_url(url, file_path)
|
|
85
|
+
File.open(file_path, "rb") do |file|
|
|
86
|
+
response = HTTP.put(url, body: file)
|
|
87
|
+
|
|
88
|
+
unless response.status.success?
|
|
89
|
+
raise BackupError, "Upload failed with status #{response.status}: #{response.body}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/bard/api.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: bard-api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Micah Geisel
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2025-12-07 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: jwt
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.7'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.7'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: http
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: backhoe
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.10'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.10'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rack-test
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '2.1'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.1'
|
|
82
|
+
description: Rack app that mounts in Rails projects to expose management endpoints
|
|
83
|
+
for BARD Tracker
|
|
84
|
+
email:
|
|
85
|
+
- micah@botandrose.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- ".rspec"
|
|
91
|
+
- LICENSE.txt
|
|
92
|
+
- README.md
|
|
93
|
+
- Rakefile
|
|
94
|
+
- config.ru
|
|
95
|
+
- lib/bard/api.rb
|
|
96
|
+
- lib/bard/api/app.rb
|
|
97
|
+
- lib/bard/api/auth.rb
|
|
98
|
+
- lib/bard/api/backup.rb
|
|
99
|
+
- lib/bard/api/version.rb
|
|
100
|
+
homepage: https://github.com/botandrose/bard-api
|
|
101
|
+
licenses:
|
|
102
|
+
- MIT
|
|
103
|
+
metadata:
|
|
104
|
+
homepage_uri: https://github.com/botandrose/bard-api
|
|
105
|
+
source_code_uri: https://github.com/botandrose/bard-api
|
|
106
|
+
rdoc_options: []
|
|
107
|
+
require_paths:
|
|
108
|
+
- lib
|
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: 3.1.0
|
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0'
|
|
119
|
+
requirements: []
|
|
120
|
+
rubygems_version: 3.6.2
|
|
121
|
+
specification_version: 4
|
|
122
|
+
summary: REST API for BARD-managed Rails projects
|
|
123
|
+
test_files: []
|