ejson_wrapper 0.3.1
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/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/README.md +80 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ejson_wrapper.gemspec +36 -0
- data/exe/ejson_wrapper +74 -0
- data/lib/ejson_wrapper/decrypt_ejson_file.rb +46 -0
- data/lib/ejson_wrapper/decrypt_private_key_with_kms.rb +28 -0
- data/lib/ejson_wrapper/generate.rb +46 -0
- data/lib/ejson_wrapper/version.rb +3 -0
- data/lib/ejson_wrapper.rb +17 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4da8151cb31c1a65a18b01e0e16833567bccd5d44c5bd31a9a0bd03e51b363ff
|
4
|
+
data.tar.gz: 297da1cbce75a75deb42110631903c342f372fb28beee2da8ace8863cefa7bcc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 444befc48197ce281f1ff2e1887375fb50aded70e4d39d84dca4749137f7a8755b0b8b2686cd2888d1395b1b2ceda7389f0ee6380071ad7c99e3471fe1e39eef
|
7
|
+
data.tar.gz: 0131ad60f86e89efa9872b310aa10557047af9e74099d1c928a20a17c832150ff4ad5ad616f7c1616abe3fcfccbeaa4e7b4ec879b28e491ddc2089118ff7d9be
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# EjsonWrapper
|
2
|
+
|
3
|
+
Wraps the EJSON go program to safely execute it and parse the resulting JSON.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'ejson_wrapper'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install ejson_wrapper
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### Decrypting EJSON files
|
24
|
+
|
25
|
+
From Ruby:
|
26
|
+
|
27
|
+
```
|
28
|
+
# Private key is in /opt/ejson/keys
|
29
|
+
EJSONWrapper.decrypt('myfile.ejson')
|
30
|
+
=> { :my_api_key => 'secret' }
|
31
|
+
|
32
|
+
# Private key is in /alternate/key/dir
|
33
|
+
EJSONWrapper.decrypt('myfile.ejson', key_dir: 'alternate/key/dir')
|
34
|
+
=> { :my_api_key => 'secret' }
|
35
|
+
|
36
|
+
# Private key is in memory
|
37
|
+
EJSONWrapper.decrypt('myfile.ejson', private_key: 'be8597abaa68bbfa23193624b1ed5e2cd6b9a8015e722138b23ecd3c90239b2d')
|
38
|
+
=> { :my_api_key => 'secret' }
|
39
|
+
|
40
|
+
# Private key is stored inside the ejson file itself as _private_key_enc (encrypted with KMS & Base64 encoded)
|
41
|
+
EJSONWrapper.decrypt('myfile.ejson', use_kms: true, region: 'ap-southeast-2')
|
42
|
+
=> { :my_api_key => 'secret' }
|
43
|
+
```
|
44
|
+
|
45
|
+
Command line:
|
46
|
+
|
47
|
+
```
|
48
|
+
# decrypt all
|
49
|
+
$ ejson_wrapper decrypt --file file.ejson --region us-east-1
|
50
|
+
{
|
51
|
+
"datadog_api_token": "[datadog_api_token]"
|
52
|
+
}
|
53
|
+
|
54
|
+
# decrypt & extract a specific secret
|
55
|
+
$ ejson_wrapper decrypt --file file.ejson --region us-east-1 --secret datadog_api_token
|
56
|
+
[datadog_api_token]
|
57
|
+
```
|
58
|
+
|
59
|
+
### Generating EJSON files
|
60
|
+
|
61
|
+
```
|
62
|
+
$ ejson_wrapper generate --region ap-southeast-2 --kms-key-id [key_id] --file file.ejson
|
63
|
+
Generated EJSON file file.ejson
|
64
|
+
|
65
|
+
$ cat file.ejson
|
66
|
+
{
|
67
|
+
"_public_key": "[public_key]",
|
68
|
+
"_private_key_enc":"[encrypted_private_key]"
|
69
|
+
}
|
70
|
+
```
|
71
|
+
|
72
|
+
## Development
|
73
|
+
|
74
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
75
|
+
|
76
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/envato/ejson_wrapper.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "ejson_wrapper"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "ejson_wrapper/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ejson_wrapper"
|
8
|
+
spec.version = EjsonWrapper::VERSION
|
9
|
+
spec.authors = ["Steve Hodgkiss"]
|
10
|
+
spec.email = ["steve@hodgkiss.me"]
|
11
|
+
|
12
|
+
spec.summary = %q{Invoke EJSON from Ruby}
|
13
|
+
spec.description = %q{Invoke EJSON from Ruby}
|
14
|
+
spec.homepage = "https://github.com/envato/ejson_wrapper"
|
15
|
+
|
16
|
+
if spec.respond_to?(:metadata)
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
else
|
19
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
20
|
+
"public gem pushes."
|
21
|
+
end
|
22
|
+
|
23
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
24
|
+
f.match(%r{^(test|spec|features)/})
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "ejson"
|
31
|
+
spec.add_dependency "aws-sdk-kms"
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
35
|
+
spec.add_development_dependency "pry"
|
36
|
+
end
|
data/exe/ejson_wrapper
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "ejson_wrapper"
|
5
|
+
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
options = {
|
9
|
+
region: nil,
|
10
|
+
kms_key_id: nil
|
11
|
+
}
|
12
|
+
option_parser = OptionParser.new do |opts|
|
13
|
+
opts.banner = 'Usage: ejson_wrapper generate [options]'
|
14
|
+
|
15
|
+
opts.on('--region R', String, 'AWS Region') do |v|
|
16
|
+
options[:region] = v
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on('--kms-key-id K', String, 'KMS Key ID') do |v|
|
20
|
+
options[:kms_key_id] = v
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on('--file F', String, 'EJSON file to read or write') do |v|
|
24
|
+
options[:file] = v
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('--secret S', String, 'Secret to extract') do |v|
|
28
|
+
options[:secret] = v
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
command = ARGV[0]
|
33
|
+
|
34
|
+
option_parser.parse!
|
35
|
+
|
36
|
+
if options[:region].nil?
|
37
|
+
STDERR.puts "Missing --region option"
|
38
|
+
STDERR.puts option_parser
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
|
42
|
+
if options[:file].nil?
|
43
|
+
STDERR.puts "Missing --file option"
|
44
|
+
STDERR.puts option_parser
|
45
|
+
exit 1
|
46
|
+
end
|
47
|
+
|
48
|
+
case command
|
49
|
+
when 'generate'
|
50
|
+
if options[:kms_key_id].nil?
|
51
|
+
STDERR.puts "Missing --kms-key-id option"
|
52
|
+
STDERR.puts option_parser
|
53
|
+
exit 1
|
54
|
+
end
|
55
|
+
|
56
|
+
EJSONWrapper.generate(region: options[:region],
|
57
|
+
kms_key_id: options[:kms_key_id],
|
58
|
+
file: options[:file])
|
59
|
+
when 'decrypt'
|
60
|
+
decrypted_secrets = EJSONWrapper.decrypt(options[:file], use_kms: true, region: options[:region])
|
61
|
+
if options[:secret]
|
62
|
+
secret = options[:secret].to_sym
|
63
|
+
unless decrypted_secrets.key?(secret)
|
64
|
+
STDERR.puts "Secret not found"
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
puts decrypted_secrets.fetch(secret)
|
68
|
+
else
|
69
|
+
puts JSON.pretty_generate(decrypted_secrets)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
STDERR.puts option_parser.banner
|
73
|
+
exit 1
|
74
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module EJSONWrapper
|
4
|
+
DecryptionFailed = Class.new(StandardError)
|
5
|
+
|
6
|
+
class DecryptEJSONFile
|
7
|
+
def self.call(file_path, **args)
|
8
|
+
new.call(file_path, **args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(file_path, key_dir: nil, private_key: nil)
|
12
|
+
decrypted_json = invoke_decrypt(file_path, key_dir: key_dir, private_key: private_key)
|
13
|
+
parse_json(decrypted_json)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def invoke_decrypt(file_path, key_dir:, private_key:)
|
19
|
+
command = ['ejson', 'decrypt']
|
20
|
+
options = {}
|
21
|
+
if private_key
|
22
|
+
options[:stdin_data] = private_key
|
23
|
+
command << '--key-from-stdin'
|
24
|
+
end
|
25
|
+
command << file_path.to_s
|
26
|
+
stdout, status = Open3.capture2(ejson_env(key_dir), *command, options)
|
27
|
+
raise DecryptionFailed, stdout unless status.success?
|
28
|
+
stdout
|
29
|
+
end
|
30
|
+
|
31
|
+
def ejson_env(key_dir)
|
32
|
+
{
|
33
|
+
'EJSON_KEYDIR' => key_dir
|
34
|
+
}.select { |_, v| !v.nil? }
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_json(decrypted_json)
|
38
|
+
JSON.parse(decrypted_json, symbolize_names: true).tap do |secrets|
|
39
|
+
secrets.delete(:_public_key)
|
40
|
+
secrets.delete(:_private_key_enc)
|
41
|
+
end.freeze
|
42
|
+
rescue JSON::ParserError
|
43
|
+
raise DecryptionFailed, "Failed to parse JSON output from EJSON"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'aws-sdk-kms'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module EJSONWrapper
|
5
|
+
PrivateKeyNotFound = Class.new(StandardError)
|
6
|
+
|
7
|
+
class DecryptPrivateKeyWithKMS
|
8
|
+
def self.call(*args)
|
9
|
+
new.call(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
KEY = '_private_key_enc'
|
13
|
+
|
14
|
+
def call(ejson_file_path, region:)
|
15
|
+
ejson_hash = JSON.parse(File.read(ejson_file_path))
|
16
|
+
encrypted_private_key = ejson_hash.fetch(KEY) do
|
17
|
+
raise PrivateKeyNotFound, "Private key was not found in ejson file under key #{key}"
|
18
|
+
end
|
19
|
+
decrypt(Base64.decode64(encrypted_private_key), region: region)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def decrypt(ciphertext_blob, region:)
|
25
|
+
Aws::KMS::Client.new(region: region).decrypt(ciphertext_blob: ciphertext_blob).plaintext
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'aws-sdk-kms'
|
3
|
+
|
4
|
+
module EJSONWrapper
|
5
|
+
KeygenFailed = Class.new(StandardError)
|
6
|
+
|
7
|
+
class Generate
|
8
|
+
def call(region:, kms_key_id:, file:)
|
9
|
+
public_key, private_key = *keygen
|
10
|
+
encrypted_private_key = encrypt_with_kms_key(region, kms_key_id, private_key)
|
11
|
+
ejson_file = JSON.pretty_generate(
|
12
|
+
'_public_key' => public_key,
|
13
|
+
'_private_key_enc' => encrypted_private_key
|
14
|
+
)
|
15
|
+
File.write(file, ejson_file)
|
16
|
+
puts "Generated EJSON file #{file}"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def keygen
|
22
|
+
output = invoke_ejson_keygen
|
23
|
+
extract_keys(output)
|
24
|
+
end
|
25
|
+
|
26
|
+
def invoke_ejson_keygen
|
27
|
+
stdout, status = Open3.capture2e('ejson', 'keygen')
|
28
|
+
raise KeygenFailed, stdout unless status.success?
|
29
|
+
stdout
|
30
|
+
end
|
31
|
+
|
32
|
+
def extract_keys(output)
|
33
|
+
lines = output.split("\n")
|
34
|
+
[lines[1], lines[3]]
|
35
|
+
end
|
36
|
+
|
37
|
+
def encrypt_with_kms_key(region, key_id, plaintext)
|
38
|
+
client = Aws::KMS::Client.new(region: region)
|
39
|
+
response = client.encrypt(
|
40
|
+
key_id: key_id,
|
41
|
+
plaintext: plaintext
|
42
|
+
)
|
43
|
+
Base64.encode64(response.ciphertext_blob).strip
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "ejson_wrapper/version"
|
2
|
+
require "ejson_wrapper/decrypt_private_key_with_kms"
|
3
|
+
require "ejson_wrapper/decrypt_ejson_file"
|
4
|
+
require "ejson_wrapper/generate"
|
5
|
+
|
6
|
+
module EJSONWrapper
|
7
|
+
def self.decrypt(file_path, key_dir: nil, private_key: nil, use_kms: false, region: nil)
|
8
|
+
if use_kms
|
9
|
+
private_key = DecryptPrivateKeyWithKMS.call(file_path, region: region)
|
10
|
+
end
|
11
|
+
DecryptEJSONFile.call(file_path, key_dir: key_dir, private_key: private_key)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.generate(**args)
|
15
|
+
Generate.new.call(**args)
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ejson_wrapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Steve Hodgkiss
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-09-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ejson
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk-kms
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.15'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.15'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Invoke EJSON from Ruby
|
98
|
+
email:
|
99
|
+
- steve@hodgkiss.me
|
100
|
+
executables:
|
101
|
+
- ejson_wrapper
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- ".rspec"
|
107
|
+
- ".travis.yml"
|
108
|
+
- Gemfile
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- bin/console
|
112
|
+
- bin/setup
|
113
|
+
- ejson_wrapper.gemspec
|
114
|
+
- exe/ejson_wrapper
|
115
|
+
- lib/ejson_wrapper.rb
|
116
|
+
- lib/ejson_wrapper/decrypt_ejson_file.rb
|
117
|
+
- lib/ejson_wrapper/decrypt_private_key_with_kms.rb
|
118
|
+
- lib/ejson_wrapper/generate.rb
|
119
|
+
- lib/ejson_wrapper/version.rb
|
120
|
+
homepage: https://github.com/envato/ejson_wrapper
|
121
|
+
licenses: []
|
122
|
+
metadata:
|
123
|
+
allowed_push_host: https://rubygems.org
|
124
|
+
post_install_message:
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 2.7.6
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: Invoke EJSON from Ruby
|
144
|
+
test_files: []
|