e3db 1.0.0 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +15 -1
- data/.yardopts +1 -0
- data/Gemfile +1 -1
- data/README.md +47 -11
- data/e3db.gemspec +1 -0
- data/examples/simple.rb +148 -0
- data/lib/e3db/client.rb +233 -53
- data/lib/e3db/config.rb +3 -3
- data/lib/e3db/crypto.rb +48 -40
- data/lib/e3db/version.rb +1 -1
- data/travis-install-configfile.sh +24 -0
- data/travis-install-libsodium.sh +18 -0
- metadata +51 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4654bc1065622f2d3f1ad6fd263688780063f4c5
|
4
|
+
data.tar.gz: 0be6154d88d0cd709a7fca0e67013cc05ad7d1fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1401984cc27238a0a168b7e71265bbdfe4b44d225b7e4edfede2519e4cd384a699f9fbd984ec61932ee7d0cc0347704bedcd08002a16081eb96d703b254324c3
|
7
|
+
data.tar.gz: 30ff287c9585276086c4fb9a7a936e3e20b9656dc5f410016b70c3833af4e29ade36207ad49e6c95db172d780b7c2abffaad261b53e3427a912c4c76750a6dfc
|
data/.travis.yml
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
sudo: false
|
2
2
|
language: ruby
|
3
3
|
rvm:
|
4
|
-
- 2.
|
4
|
+
- 2.3.4
|
5
|
+
- 2.4.1
|
6
|
+
|
7
|
+
cache:
|
8
|
+
bundler: true
|
9
|
+
directories:
|
10
|
+
- $HOME/libsodium
|
11
|
+
|
5
12
|
before_install: gem install bundler -v 1.12.3
|
13
|
+
|
14
|
+
install:
|
15
|
+
- ./travis-install-libsodium.sh
|
16
|
+
- ./travis-install-configfile.sh
|
17
|
+
- export PKG_CONFIG_PATH=$HOME/libsodium/lib/pkgconfig:$PKG_CONFIG_PATH
|
18
|
+
- export LD_LIBRARY_PATH=$HOME/libsodium/lib:$LD_LIBRARY_PATH
|
19
|
+
- bundle install
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
[![Gem Version][gem-image]][gem-url] [![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url]
|
1
2
|
|
2
3
|
# Introduction
|
3
4
|
|
@@ -83,19 +84,18 @@ client = E3DB::Client.new(config)
|
|
83
84
|
|
84
85
|
## Writing a record
|
85
86
|
|
86
|
-
To write new records to the database,
|
87
|
-
|
88
|
-
the fields of the record
|
89
|
-
|
90
|
-
unique ID of the newly created record.
|
87
|
+
To write new records to the database, call the `E3DB::Client#write`
|
88
|
+
method with a string describing the type of data to be written,
|
89
|
+
along with a hash containing the fields of the record. `E3DB::Client#write`
|
90
|
+
returns the newly created record.
|
91
91
|
|
92
92
|
```ruby
|
93
|
-
record = client.
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
printf("Wrote record %s\n", record_id)
|
93
|
+
record = client.write('contact', {
|
94
|
+
:first_name => 'Jon',
|
95
|
+
:last_name => 'Snow',
|
96
|
+
:phone => '555-555-1212'
|
97
|
+
})
|
98
|
+
printf("Wrote record %s\n", record.meta.record_id)
|
99
99
|
```
|
100
100
|
|
101
101
|
## Querying Records
|
@@ -114,6 +114,29 @@ client.query(type: 'contact') do |record|
|
|
114
114
|
end
|
115
115
|
```
|
116
116
|
|
117
|
+
In this example, the `E3DB::Client#query` method takes a block that will
|
118
|
+
execute for each record that matches the query. Records will be streamed
|
119
|
+
efficiently from the server in batches, allowing processing of large data
|
120
|
+
sets without consuming excessive memory.
|
121
|
+
|
122
|
+
In some cases, it is more convenient to load all results into memory
|
123
|
+
for processing. To achieve this, instead of passing a block to
|
124
|
+
`E3DB::Client#query`, you can call `Enumerable` methods on the query result,
|
125
|
+
including `Enumerable#to_a` to convert the results to an array.
|
126
|
+
|
127
|
+
For example:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
results = client.query(type: 'contact').to_a
|
131
|
+
printf("There were %d results.\n", results.length)
|
132
|
+
results.each do |record|
|
133
|
+
puts record
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
## More examples
|
138
|
+
See the [simple example code](examples/simple.rb) for runnable detailed examples.
|
139
|
+
|
117
140
|
## Development
|
118
141
|
|
119
142
|
Before running tests, register an `integration-test` profile using
|
@@ -133,6 +156,12 @@ then run `bundle exec rake release`, which will create a git tag for the
|
|
133
156
|
version, push git commits and tags, and push the `.gem` file to
|
134
157
|
[rubygems.org](https://rubygems.org).
|
135
158
|
|
159
|
+
## Documentation
|
160
|
+
|
161
|
+
General E3DB documentation is [on our web site](https://tozny.com/documentation/e3db/).
|
162
|
+
|
163
|
+
Comprehensive documentation for the SDK can be found online [via RubyDoc.info](http://www.rubydoc.info/gems/e3db/1.0.0).
|
164
|
+
|
136
165
|
## Contributing
|
137
166
|
|
138
167
|
Bug reports and pull requests are welcome on GitHub at https://github.com/tozny/e3db-ruby.
|
@@ -140,3 +169,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/tozny/
|
|
140
169
|
## License
|
141
170
|
|
142
171
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
172
|
+
|
173
|
+
[gem-image]: https://badge.fury.io/rb/e3db.svg
|
174
|
+
[gem-url]: https://rubygems.org/gems/e3db
|
175
|
+
[travis-image]: https://travis-ci.org/tozny/e3db-ruby.svg?branch=master
|
176
|
+
[travis-url]: https://travis-ci.org/tozny/e3db-ruby
|
177
|
+
[coveralls-image]: https://coveralls.io/repos/github/tozny/e3db-ruby/badge.svg?branch=master
|
178
|
+
[coveralls-url]: https://coveralls.io/github/tozny/e3db-ruby
|
data/e3db.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
23
|
spec.add_development_dependency "rspec", "~> 3.0"
|
24
24
|
spec.add_development_dependency 'simplecov', '~> 0.14.1'
|
25
|
+
spec.add_development_dependency 'coveralls', '~> 0.8.0'
|
25
26
|
|
26
27
|
spec.add_dependency 'dry-struct', '~> 0.2.1'
|
27
28
|
spec.add_dependency 'lru_redux', '~> 1.1'
|
data/examples/simple.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# This program provides a few simple examples of reading, writing, and
|
2
|
+
# querying e3db records. For more detailed information, please see the
|
3
|
+
# documentation home page: https://tozny.com/documentation/e3db/
|
4
|
+
#
|
5
|
+
# Author:: Isaac Potoczny-Jones (mailto:ijones@tozny.com)
|
6
|
+
# Copyright:: Copyright (c) 2017 Tozny, LLC
|
7
|
+
# License:: Public Domain
|
8
|
+
|
9
|
+
# ---------------------------------------------------------
|
10
|
+
# Initialization
|
11
|
+
# ---------------------------------------------------------
|
12
|
+
|
13
|
+
require 'e3db'
|
14
|
+
|
15
|
+
# Configuration files live in ~/.tozny and you can have several
|
16
|
+
# different "profiles" like *dev* and *production*.
|
17
|
+
config = E3DB::Config.default
|
18
|
+
|
19
|
+
# Now create a client using that configuration.
|
20
|
+
client = E3DB::Client.new(config)
|
21
|
+
|
22
|
+
# ---------------------------------------------------------
|
23
|
+
# Writing a record
|
24
|
+
# ---------------------------------------------------------
|
25
|
+
|
26
|
+
# Create a record by first creating a local version as a map:
|
27
|
+
data = {
|
28
|
+
:name => 'Jon Snow',
|
29
|
+
:what_he_knows => 'Nothing'
|
30
|
+
}
|
31
|
+
|
32
|
+
# Now encrypt the *value* part of the record, write it to the server and
|
33
|
+
# the server returns the newly created record:
|
34
|
+
record = client.write('test-contact', data)
|
35
|
+
record_id = record.meta.record_id
|
36
|
+
puts("Wrote: " + record_id)
|
37
|
+
|
38
|
+
# ---------------------------------------------------------
|
39
|
+
# Simple reading and queries
|
40
|
+
# ---------------------------------------------------------
|
41
|
+
|
42
|
+
# Use the new record's unique ID to read the same record again from E3DB:
|
43
|
+
newRecord = client.read(record.meta.record_id)
|
44
|
+
puts 'Record: ' + newRecord.data[:name] + ' ' + record.data[:what_he_knows]
|
45
|
+
|
46
|
+
# Query for all records of type 'test-contact' and print out
|
47
|
+
# a little bit of data and metadata.
|
48
|
+
client.query(type: 'test-contact').each do |record|
|
49
|
+
puts 'Data: ' + record.data[:name] + ' ' + record.data[:what_he_knows]
|
50
|
+
puts 'Metadata: ' + record.meta.record_id + ' ' + record.meta.type
|
51
|
+
end
|
52
|
+
|
53
|
+
# ---------------------------------------------------------
|
54
|
+
# Simple sharing by record type
|
55
|
+
# ---------------------------------------------------------
|
56
|
+
|
57
|
+
# Share all of the records of type 'test-contact' with Isaac's client ID:
|
58
|
+
isaac_client_id = 'db1744b9-3fb6-4458-a291-0bc677dba08b'
|
59
|
+
client.share('test-contact', isaac_client_id)
|
60
|
+
|
61
|
+
# Share all of the records of type 'test-contact' with Isaac's email address.
|
62
|
+
# This only works if the client has opted into discovery of their client_id.
|
63
|
+
client.share('test-contact', 'ijones+feedback@tozny.com')
|
64
|
+
|
65
|
+
# ---------------------------------------------------------
|
66
|
+
# More complex queries
|
67
|
+
# ---------------------------------------------------------
|
68
|
+
|
69
|
+
# Create some new records of the same type (note that they are also shared
|
70
|
+
# automatically since they are a type that we have shared above. We
|
71
|
+
# will also add some "plain" fields that are not secret but can be used
|
72
|
+
# for efficient querying:
|
73
|
+
|
74
|
+
bran_data = { :name => 'Bran', :what_he_knows => 'Crow' }
|
75
|
+
bran_plain = { :house => 'Stark', :ageRange => 'child' }
|
76
|
+
client.write('test-contact', bran_data, bran_plain)
|
77
|
+
|
78
|
+
hodor_data = { :name => 'Hodor', :what_he_knows => 'Hodor' }
|
79
|
+
hodor_plain = { :house => 'Stark', :ageRange => 'adult' }
|
80
|
+
client.write('test-contact', hodor_data, hodor_plain)
|
81
|
+
|
82
|
+
doran_data = { :name => 'Doran', :what_he_knows => 'Oberyn' }
|
83
|
+
doran_plain = { :house => 'Martell', :ageRange => 'adult' }
|
84
|
+
client.write('test-contact', doran_data, doran_plain)
|
85
|
+
|
86
|
+
# Create a query that finds everyone from house Stark, but not others:
|
87
|
+
queryWesteros = Hash.new
|
88
|
+
queryWesteros = {:eq => {:name => 'house', :value => 'Stark'} }
|
89
|
+
|
90
|
+
# Execute that query:
|
91
|
+
client.query(plain: queryWesteros).each do |record|
|
92
|
+
puts record.data[:name]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Now create a more complex query with only the adults from house Stark:
|
96
|
+
queryWesteros = {:and => [
|
97
|
+
{:eq => {:name => 'house', :value => 'Stark'} },
|
98
|
+
{:eq => {:name => 'ageRange', :value => 'adult'} }
|
99
|
+
]}
|
100
|
+
|
101
|
+
# Execute that query:
|
102
|
+
client.query(plain: queryWesteros) do |record|
|
103
|
+
puts record.data[:name]
|
104
|
+
end
|
105
|
+
|
106
|
+
# ---------------------------------------------------------
|
107
|
+
# Learning about other clients
|
108
|
+
# ---------------------------------------------------------
|
109
|
+
isaac_client_info = client.client_info('ijones+feedback@tozny.com')
|
110
|
+
puts isaac_client_info.inspect
|
111
|
+
|
112
|
+
# Fetch the public key:
|
113
|
+
isaac_pub_key = client.client_key(isaac_client_id)
|
114
|
+
puts isaac_pub_key.inspect
|
115
|
+
|
116
|
+
# ---------------------------------------------------------
|
117
|
+
# More reading and inspection of records
|
118
|
+
# ---------------------------------------------------------
|
119
|
+
|
120
|
+
# read_raw gets a record without decrypting its data
|
121
|
+
rawRecord = client.read_raw(record_id)
|
122
|
+
newRecord = client.read(record_id)
|
123
|
+
|
124
|
+
# So let's compare them:
|
125
|
+
|
126
|
+
puts (rawRecord.meta == newRecord.meta).to_s # true
|
127
|
+
puts (rawRecord.data == newRecord.data).to_s # false
|
128
|
+
|
129
|
+
puts newRecord.data[:name] + ' encrypts to ' + rawRecord.data[:name]
|
130
|
+
|
131
|
+
# Records contain a few other fields that are fun to look at, and this gives
|
132
|
+
# you a good sense for what's encrypted and what's not:
|
133
|
+
puts rawRecord.inspect
|
134
|
+
|
135
|
+
# ---------------------------------------------------------
|
136
|
+
# Clean up - Comment these out if you want to experiment
|
137
|
+
# ---------------------------------------------------------
|
138
|
+
|
139
|
+
# Revoke the sharing created by the client.share
|
140
|
+
client.revoke('test-contact', 'ijones+feedback@tozny.com')
|
141
|
+
|
142
|
+
# Delete the record we created above
|
143
|
+
client.delete(record_id)
|
144
|
+
|
145
|
+
# Delete all of the records of type test-contact from previous runs:
|
146
|
+
client.query(type: 'test-contact') do |record|
|
147
|
+
client.delete(record.meta.record_id)
|
148
|
+
end
|
data/lib/e3db/client.rb
CHANGED
@@ -35,6 +35,23 @@ module E3DB
|
|
35
35
|
|
36
36
|
private_constant :TokenHelper
|
37
37
|
|
38
|
+
# Exception thrown by {Client#update} when a concurrent modification
|
39
|
+
# is detected. Upon catching this exception, a client should re-fetch
|
40
|
+
# the affected record and retry the update operation.
|
41
|
+
class ConflictError < StandardError
|
42
|
+
def initialize(record)
|
43
|
+
super('Conflict updating record: ' + record.meta.record_id)
|
44
|
+
@record = record
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the record from the failing update attempt.
|
48
|
+
#
|
49
|
+
# @return [Record] the affected record
|
50
|
+
def record
|
51
|
+
@record
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
38
55
|
# A client's public key information.
|
39
56
|
#
|
40
57
|
# @!attribute curve25519
|
@@ -72,6 +89,8 @@ module E3DB
|
|
72
89
|
# @return [Time, nil] when this record was created, or nil if unavailable
|
73
90
|
# @!attribute last_modified
|
74
91
|
# @return [Time, nil] when this record was last modified, or nil if unavailable
|
92
|
+
# @!attribute version
|
93
|
+
# @return [String] opaque version identifier updated by server on changes
|
75
94
|
class Meta < Dry::Struct
|
76
95
|
attribute :record_id, Types::Strict::String.optional
|
77
96
|
attribute :writer_id, Types::Strict::String
|
@@ -80,6 +99,7 @@ module E3DB
|
|
80
99
|
attribute :plain, Types::Strict::Hash.default { Hash.new }
|
81
100
|
attribute :created, Types::Json::DateTime.optional
|
82
101
|
attribute :last_modified, Types::Json::DateTime.optional
|
102
|
+
attribute :version, Types::Strict::String.optional
|
83
103
|
end
|
84
104
|
|
85
105
|
# A E3DB record containing data and metadata. Records are
|
@@ -88,9 +108,8 @@ module E3DB
|
|
88
108
|
# to the server for storage, and decrypted in the client after
|
89
109
|
# they are read.
|
90
110
|
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
# {Client#write}.
|
111
|
+
# New records are written to the database by calling the
|
112
|
+
# {Client#write} method.
|
94
113
|
#
|
95
114
|
# To read a record by their unique ID, use {Client#read}, or to
|
96
115
|
# query a set of records based on their attributes, use {Client#query}.
|
@@ -102,6 +121,49 @@ module E3DB
|
|
102
121
|
class Record < Dry::Struct
|
103
122
|
attribute :meta, Meta
|
104
123
|
attribute :data, Types::Strict::Hash.default { Hash.new }
|
124
|
+
|
125
|
+
# Allow updating metadata, used on destructive update.
|
126
|
+
def meta=(meta)
|
127
|
+
@meta = meta
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Information about records shared with this client.
|
132
|
+
#
|
133
|
+
# The {Client#incoming_sharing} method returns a list of
|
134
|
+
# {IncomingSharingPolicy} instances, each of which describes
|
135
|
+
# a rule allowing this client to read records of a specific
|
136
|
+
# type, written by another client.
|
137
|
+
#
|
138
|
+
# @!attribute writer_id
|
139
|
+
# @return [String] unique ID of the writer that shared with this client
|
140
|
+
# @!attribute writer_name
|
141
|
+
# @return [String] display name of the writer, if available
|
142
|
+
# @!attribute record_type
|
143
|
+
# @return [String] type of record shared with this client
|
144
|
+
class IncomingSharingPolicy < Dry::Struct
|
145
|
+
attribute :writer_id, Types::Strict::String
|
146
|
+
attribute :writer_name, Types::Strict::String.optional
|
147
|
+
attribute :record_type, Types::Strict::String
|
148
|
+
end
|
149
|
+
|
150
|
+
# Information about records shared with another client.
|
151
|
+
#
|
152
|
+
# The {Client#outgoing_sharing} method returns a list of
|
153
|
+
# {OutgoingSharingPolicy} instances, each of which describes
|
154
|
+
# a rule allowing other E3DB clients to read records of a
|
155
|
+
# specific type.
|
156
|
+
#
|
157
|
+
# @!attribute reader_id
|
158
|
+
# @return [String] unique ID of the authorized reader
|
159
|
+
# @!attribute reader_name
|
160
|
+
# @return [String] display name of reader, if available
|
161
|
+
# @!attribute record_type
|
162
|
+
# @return [String] type of record shared with reader
|
163
|
+
class OutgoingSharingPolicy < Dry::Struct
|
164
|
+
attribute :reader_id, Types::Strict::String
|
165
|
+
attribute :reader_name, Types::Strict::String.optional
|
166
|
+
attribute :record_type, Types::Strict::String
|
105
167
|
end
|
106
168
|
|
107
169
|
# A connection to the E3DB service used to perform database operations.
|
@@ -146,10 +208,17 @@ module E3DB
|
|
146
208
|
|
147
209
|
# Query the server for information about an E3DB client.
|
148
210
|
#
|
149
|
-
# @param client_id [String] client ID to look up
|
211
|
+
# @param client_id [String] client ID or e-mail address to look up
|
150
212
|
# @return [ClientInfo] information about this client
|
151
213
|
def client_info(client_id)
|
152
|
-
|
214
|
+
if client_id.include? "@"
|
215
|
+
base_url = get_url('v1', 'storage', 'clients', 'find')
|
216
|
+
url = base_url + sprintf('?email=%s', CGI.escape(client_id))
|
217
|
+
resp = @conn.post(url)
|
218
|
+
else
|
219
|
+
resp = @conn.get(get_url('v1', 'storage', 'clients', client_id))
|
220
|
+
end
|
221
|
+
|
153
222
|
ClientInfo.new(JSON.parse(resp.body, symbolize_names: true))
|
154
223
|
end
|
155
224
|
|
@@ -184,30 +253,48 @@ module E3DB
|
|
184
253
|
decrypt_record(read_raw(record_id))
|
185
254
|
end
|
186
255
|
|
187
|
-
#
|
188
|
-
# by calling {Client#write}.
|
256
|
+
# Write a new record to the E3DB storage service.
|
189
257
|
#
|
190
|
-
# @param type [String] free-form content type of this record
|
191
|
-
# @
|
192
|
-
|
258
|
+
# @param type [String] free-form content type name of this record
|
259
|
+
# @param data [Hash<String, String>] record data to be stored encrypted
|
260
|
+
# @param plain [Hash<String, String>] record data to be stored unencrypted for querying
|
261
|
+
# @return [Record] the newly created record object
|
262
|
+
def write(type, data, plain=Hash.new)
|
263
|
+
url = get_url('v1', 'storage', 'records')
|
193
264
|
id = @config.client_id
|
194
265
|
meta = Meta.new(record_id: nil, writer_id: id, user_id: id,
|
195
|
-
type: type, plain:
|
196
|
-
last_modified: nil)
|
197
|
-
Record.new(meta: meta, data:
|
266
|
+
type: type, plain: plain, created: nil,
|
267
|
+
last_modified: nil, version: nil)
|
268
|
+
record = Record.new(meta: meta, data: data)
|
269
|
+
resp = @conn.post(url, encrypt_record(record).to_hash)
|
270
|
+
decrypt_record(Record.new(JSON.parse(resp.body, symbolize_names: true)))
|
198
271
|
end
|
199
272
|
|
200
|
-
#
|
273
|
+
# Update an existing record in the E3DB storage service.
|
201
274
|
#
|
202
|
-
#
|
275
|
+
# If the record has been modified by another client since it was
|
276
|
+
# read, this method raises {ConflictError}, which should be caught
|
277
|
+
# by the caller so that the record can be re-fetched and the update retried.
|
203
278
|
#
|
204
|
-
#
|
205
|
-
#
|
206
|
-
|
207
|
-
|
208
|
-
|
279
|
+
# The metadata of the input record will be updated in-place to reflect
|
280
|
+
# the new version number and modification time returned by the server.
|
281
|
+
#
|
282
|
+
# @param record [Record] the record to update
|
283
|
+
def update(record)
|
284
|
+
record_id = record.meta.record_id
|
285
|
+
version = record.meta.version
|
286
|
+
url = get_url('v1', 'storage', 'records', 'safe', record_id, version)
|
287
|
+
begin
|
288
|
+
resp = @conn.put(url, encrypt_record(record).to_hash)
|
289
|
+
rescue Faraday::ClientError => e
|
290
|
+
if e.response[:status] == 409
|
291
|
+
raise E3DB::ConflictError, record
|
292
|
+
else
|
293
|
+
raise e # re-raise on other failures
|
294
|
+
end
|
295
|
+
end
|
209
296
|
json = JSON.parse(resp.body, symbolize_names: true)
|
210
|
-
json[:meta]
|
297
|
+
record.meta = Meta.new(json[:meta])
|
211
298
|
end
|
212
299
|
|
213
300
|
# Delete a record from the E3DB storage service.
|
@@ -218,14 +305,15 @@ module E3DB
|
|
218
305
|
end
|
219
306
|
|
220
307
|
class Query < Dry::Struct
|
221
|
-
attribute :count,
|
222
|
-
attribute :include_data,
|
223
|
-
attribute :writer_ids,
|
224
|
-
attribute :user_ids,
|
225
|
-
attribute :record_ids,
|
226
|
-
attribute :content_types,
|
227
|
-
attribute :plain,
|
228
|
-
attribute :after_index,
|
308
|
+
attribute :count, Types::Int
|
309
|
+
attribute :include_data, Types::Bool.optional
|
310
|
+
attribute :writer_ids, Types::Coercible::Array.member(Types::String).optional
|
311
|
+
attribute :user_ids, Types::Coercible::Array.member(Types::String).optional
|
312
|
+
attribute :record_ids, Types::Coercible::Array.member(Types::String).optional
|
313
|
+
attribute :content_types, Types::Coercible::Array.member(Types::String).optional
|
314
|
+
attribute :plain, Types::Hash.optional
|
315
|
+
attribute :after_index, Types::Int.optional
|
316
|
+
attribute :include_all_writers, Types::Bool.optional
|
229
317
|
|
230
318
|
def after_index=(index)
|
231
319
|
@after_index = index
|
@@ -241,49 +329,117 @@ module E3DB
|
|
241
329
|
DEFAULT_QUERY_COUNT = 100
|
242
330
|
private_constant :DEFAULT_QUERY_COUNT
|
243
331
|
|
332
|
+
# A set of records returned by {Client#query}. This implements the
|
333
|
+
# `Enumerable` interface which can be used to loop over the records
|
334
|
+
# in the result set (using eg: `Enumerable#each`).
|
335
|
+
#
|
336
|
+
# Every traversal of the result set will execute a query to the server,
|
337
|
+
# so if multiple in-memory traversals are needed, use `Enumerable#to_a` to
|
338
|
+
# fetch all records into an array first.
|
339
|
+
class Result
|
340
|
+
include Enumerable
|
341
|
+
|
342
|
+
def initialize(client, query, raw)
|
343
|
+
@client = client
|
344
|
+
@query = query
|
345
|
+
@raw = raw
|
346
|
+
end
|
347
|
+
|
348
|
+
# Invoke a block for each record matching a query.
|
349
|
+
def each
|
350
|
+
# Every invocation of 'each' gets its own copy of the query since
|
351
|
+
# it will be modified as we loop through the result pages. This
|
352
|
+
# allows multiple traversals of the same result set to start from
|
353
|
+
# the beginning each time.
|
354
|
+
q = Query.new(@query.to_hash)
|
355
|
+
loop do
|
356
|
+
json = @client.instance_eval { query1(q) }
|
357
|
+
results = json[:results]
|
358
|
+
results.each do |r|
|
359
|
+
record = Record.new(meta: r[:meta], data: r[:record_data] || Hash.new)
|
360
|
+
if q.include_data && !@raw
|
361
|
+
access_key = r[:access_key]
|
362
|
+
if access_key
|
363
|
+
record = @client.instance_eval {
|
364
|
+
ak = decrypt_eak(access_key)
|
365
|
+
decrypt_record_with_key(record, ak)
|
366
|
+
}
|
367
|
+
else
|
368
|
+
record = @client.instance_eval { decrypt_record(record) }
|
369
|
+
end
|
370
|
+
end
|
371
|
+
yield record
|
372
|
+
end
|
373
|
+
|
374
|
+
if results.length < q.count
|
375
|
+
break
|
376
|
+
end
|
377
|
+
|
378
|
+
q.after_index = json[:last_index]
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
244
383
|
# Query E3DB records according to a set of selection criteria.
|
245
384
|
#
|
246
|
-
#
|
247
|
-
#
|
385
|
+
# The default behavior is to return all records written by the
|
386
|
+
# current authenticated client.
|
387
|
+
#
|
388
|
+
# To restrict the results to a particular type, pass a type or
|
389
|
+
# list of types as the `type` argument.
|
390
|
+
#
|
391
|
+
# To restrict the results to a set of clients, pass a single or
|
392
|
+
# list of client IDs as the `writer` argument. To list records
|
393
|
+
# written by any client that has shared with the current client,
|
394
|
+
# pass the special token `:any` as the `writer` argument.
|
248
395
|
#
|
249
|
-
#
|
396
|
+
# If a block is supplied, each record matching the query parameters
|
397
|
+
# is fetched from the server and yielded to the block.
|
398
|
+
#
|
399
|
+
# If no block is supplied, a {Result} is returned that will
|
400
|
+
# iterate over the records matching the query parameters. This
|
401
|
+
# iterator is lazy and will query the server each time it is used,
|
402
|
+
# so calling `Enumerable#to_a` to convert to an array is recommended
|
403
|
+
# if multiple traversals are necessary.
|
404
|
+
#
|
405
|
+
# @param writer [String,Array<String>,:all] select records written by these client IDs or :all for all writers
|
250
406
|
# @param record [String,Array<String>] select records with these record IDs
|
251
407
|
# @param type [String,Array<string>] select records with these types
|
252
408
|
# @param plain [Hash] plaintext query expression to select
|
253
409
|
# @param data [Boolean] include data in records
|
254
410
|
# @param raw [Boolean] when true don't decrypt record data
|
255
|
-
|
411
|
+
# @param page_size [Integer] number of records to fetch per request
|
412
|
+
# @return [Result] a result set object enumerating matched records
|
413
|
+
def query(data: true, raw: false, writer: nil, record: nil, type: nil, plain: nil, page_size: DEFAULT_QUERY_COUNT)
|
414
|
+
all_writers = false
|
415
|
+
if writer == :all
|
416
|
+
all_writers = true
|
417
|
+
writer = []
|
418
|
+
end
|
419
|
+
|
256
420
|
q = Query.new(after_index: 0, include_data: data, writer_ids: writer,
|
257
421
|
record_ids: record, content_types: type, plain: plain,
|
258
|
-
user_ids: nil, count:
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
results.each do |r|
|
265
|
-
record = Record.new(meta: r[:meta], data: r[:record_data] || Hash.new)
|
266
|
-
if data && !raw
|
267
|
-
record = decrypt_record(record)
|
268
|
-
end
|
269
|
-
yield record
|
270
|
-
end
|
271
|
-
|
272
|
-
if results.length < q.count
|
273
|
-
break
|
422
|
+
user_ids: nil, count: page_size,
|
423
|
+
include_all_writers: all_writers)
|
424
|
+
result = Result.new(self, q, raw)
|
425
|
+
if block_given?
|
426
|
+
result.each do |rec|
|
427
|
+
yield rec
|
274
428
|
end
|
275
|
-
|
276
|
-
|
429
|
+
else
|
430
|
+
result
|
277
431
|
end
|
278
432
|
end
|
279
433
|
|
280
434
|
# Grant another E3DB client access to records of a particular type.
|
281
435
|
#
|
282
436
|
# @param type [String] type of records to share
|
283
|
-
# @param reader_id [String] client ID of reader to grant access to
|
437
|
+
# @param reader_id [String] client ID or e-mail address of reader to grant access to
|
284
438
|
def share(type, reader_id)
|
285
439
|
if reader_id == @config.client_id
|
286
440
|
return
|
441
|
+
elsif reader_id.include? "@"
|
442
|
+
reader_id = client_info(reader_id).client_id
|
287
443
|
end
|
288
444
|
|
289
445
|
id = @config.client_id
|
@@ -301,6 +457,8 @@ module E3DB
|
|
301
457
|
def revoke(type, reader_id)
|
302
458
|
if reader_id == @config.client_id
|
303
459
|
return
|
460
|
+
elsif reader_id.include? "@"
|
461
|
+
reader_id = client_info(reader_id).client_id
|
304
462
|
end
|
305
463
|
|
306
464
|
id = @config.client_id
|
@@ -308,9 +466,31 @@ module E3DB
|
|
308
466
|
@conn.put(url, JSON.generate({:deny => [{:read => {}}]}))
|
309
467
|
end
|
310
468
|
|
469
|
+
def outgoing_sharing
|
470
|
+
url = get_url('v1', 'storage', 'policy', 'outgoing')
|
471
|
+
resp = @conn.get(url)
|
472
|
+
json = JSON.parse(resp.body, symbolize_names: true)
|
473
|
+
return json.map {|x| OutgoingSharingPolicy.new(x)}
|
474
|
+
end
|
475
|
+
|
476
|
+
def incoming_sharing
|
477
|
+
url = get_url('v1', 'storage', 'policy', 'incoming')
|
478
|
+
resp = @conn.get(url)
|
479
|
+
json = JSON.parse(resp.body, symbolize_names: true)
|
480
|
+
return json.map {|x| IncomingSharingPolicy.new(x)}
|
481
|
+
end
|
482
|
+
|
311
483
|
private
|
484
|
+
|
485
|
+
# Fetch a single page of query results. Used internally by {Client#query}.
|
486
|
+
def query1(query)
|
487
|
+
url = get_url('v1', 'storage', 'search')
|
488
|
+
resp = @conn.post(url, query.as_json)
|
489
|
+
return JSON.parse(resp.body, symbolize_names: true)
|
490
|
+
end
|
491
|
+
|
312
492
|
def get_url(*paths)
|
313
|
-
sprintf('%s/%s', @config.api_url.chomp('/'), paths.map { |x|
|
493
|
+
sprintf('%s/%s', @config.api_url.chomp('/'), paths.map { |x| CGI.escape x }.join('/'))
|
314
494
|
end
|
315
495
|
end
|
316
496
|
end
|
data/lib/e3db/config.rb
CHANGED
@@ -7,14 +7,14 @@
|
|
7
7
|
|
8
8
|
|
9
9
|
module E3DB
|
10
|
-
DEFAULT_API_URL = 'https://
|
10
|
+
DEFAULT_API_URL = 'https://api.e3db.com/'
|
11
11
|
|
12
12
|
# Configuration and credentials for E3DB.
|
13
13
|
#
|
14
14
|
# Typically a configuration is loaded from a JSON file generated
|
15
15
|
# during registration via the E3DB administration console
|
16
16
|
# or command-line tool. To load a configuration from a JSON file,
|
17
|
-
# use {Config.load}.
|
17
|
+
# use {E3DB::Config.load}.
|
18
18
|
#
|
19
19
|
# @!attribute version
|
20
20
|
# @return [Int] the version number of the configuration format (currently 1)
|
@@ -47,7 +47,7 @@ module E3DB
|
|
47
47
|
attribute :logging, Types::Bool
|
48
48
|
|
49
49
|
# Load configuration from a JSON file created during registration
|
50
|
-
# or with {Config.save}.
|
50
|
+
# or with {E3DB::Config.save}.
|
51
51
|
#
|
52
52
|
# The configuration file should contain a single JSON object
|
53
53
|
# with the following structure:
|
data/lib/e3db/crypto.rb
CHANGED
@@ -9,50 +9,16 @@
|
|
9
9
|
module E3DB
|
10
10
|
class Client
|
11
11
|
private
|
12
|
-
def
|
13
|
-
ak_cache_key = [writer_id, user_id, type]
|
14
|
-
if @ak_cache.key? ak_cache_key
|
15
|
-
return @ak_cache[ak_cache_key]
|
16
|
-
end
|
17
|
-
|
18
|
-
url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
|
19
|
-
resp = @conn.get(url)
|
20
|
-
json = JSON.parse(resp.body, symbolize_names: true)
|
21
|
-
|
22
|
-
k = json[:authorizer_public_key][:curve25519]
|
23
|
-
authorizer_pubkey = Crypto.decode_public_key(k)
|
24
|
-
|
25
|
-
fields = json[:eak].split('.', 2)
|
26
|
-
ciphertext = Crypto.base64decode(fields[0])
|
27
|
-
nonce = Crypto.base64decode(fields[1])
|
28
|
-
box = RbNaCl::Box.new(authorizer_pubkey, @private_key)
|
29
|
-
|
30
|
-
ak = box.decrypt(nonce, ciphertext)
|
31
|
-
@ak_cache[ak_cache_key] = ak
|
32
|
-
ak
|
33
|
-
end
|
34
|
-
|
35
|
-
def put_access_key(writer_id, user_id, reader_id, type, ak)
|
36
|
-
ak_cache_key = [writer_id, user_id, type]
|
37
|
-
@ak_cache[ak_cache_key] = ak
|
38
|
-
|
39
|
-
reader_key = client_key(reader_id)
|
40
|
-
nonce = RbNaCl::Random.random_bytes(RbNaCl::Box.nonce_bytes)
|
41
|
-
eak = RbNaCl::Box.new(reader_key, @private_key).encrypt(nonce, ak)
|
42
|
-
|
43
|
-
encoded_eak = sprintf('%s.%s', Crypto.base64encode(eak), Crypto.base64encode(nonce))
|
44
|
-
|
45
|
-
url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
|
46
|
-
@conn.put(url, { :eak => encoded_eak })
|
47
|
-
end
|
48
|
-
|
49
|
-
def decrypt_record(encrypted_record)
|
50
|
-
record = Record.new(meta: encrypted_record.meta.clone, data: Hash.new)
|
51
|
-
|
12
|
+
def decrypt_record(record)
|
52
13
|
writer_id = record.meta.writer_id
|
53
14
|
user_id = record.meta.user_id
|
54
15
|
type = record.meta.type
|
55
16
|
ak = get_access_key(writer_id, user_id, @config.client_id, type)
|
17
|
+
decrypt_record_with_key(record, ak)
|
18
|
+
end
|
19
|
+
|
20
|
+
def decrypt_record_with_key(encrypted_record, ak)
|
21
|
+
record = Record.new(meta: encrypted_record.meta.clone, data: Hash.new)
|
56
22
|
|
57
23
|
encrypted_record.data.each do |k, v|
|
58
24
|
fields = v.split('.', 4)
|
@@ -99,6 +65,48 @@ module E3DB
|
|
99
65
|
|
100
66
|
record
|
101
67
|
end
|
68
|
+
|
69
|
+
def decrypt_eak(json)
|
70
|
+
k = json[:authorizer_public_key][:curve25519]
|
71
|
+
authorizer_pubkey = Crypto.decode_public_key(k)
|
72
|
+
|
73
|
+
fields = json[:eak].split('.', 2)
|
74
|
+
ciphertext = Crypto.base64decode(fields[0])
|
75
|
+
nonce = Crypto.base64decode(fields[1])
|
76
|
+
box = RbNaCl::Box.new(authorizer_pubkey, @private_key)
|
77
|
+
|
78
|
+
box.decrypt(nonce, ciphertext)
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_access_key(writer_id, user_id, reader_id, type)
|
82
|
+
ak_cache_key = [writer_id, user_id, type]
|
83
|
+
if @ak_cache.key? ak_cache_key
|
84
|
+
return @ak_cache[ak_cache_key]
|
85
|
+
end
|
86
|
+
|
87
|
+
url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
|
88
|
+
resp = @conn.get(url)
|
89
|
+
json = JSON.parse(resp.body, symbolize_names: true)
|
90
|
+
|
91
|
+
ak = decrypt_eak(json)
|
92
|
+
@ak_cache[ak_cache_key] = ak
|
93
|
+
ak
|
94
|
+
end
|
95
|
+
|
96
|
+
def put_access_key(writer_id, user_id, reader_id, type, ak)
|
97
|
+
ak_cache_key = [writer_id, user_id, type]
|
98
|
+
@ak_cache[ak_cache_key] = ak
|
99
|
+
|
100
|
+
reader_key = client_key(reader_id)
|
101
|
+
nonce = RbNaCl::Random.random_bytes(RbNaCl::Box.nonce_bytes)
|
102
|
+
eak = RbNaCl::Box.new(reader_key, @private_key).encrypt(nonce, ak)
|
103
|
+
|
104
|
+
encoded_eak = sprintf('%s.%s', Crypto.base64encode(eak), Crypto.base64encode(nonce))
|
105
|
+
|
106
|
+
url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
|
107
|
+
@conn.put(url, { :eak => encoded_eak })
|
108
|
+
end
|
109
|
+
|
102
110
|
end
|
103
111
|
|
104
112
|
class Crypto
|
data/lib/e3db/version.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# The purpose of this file is to install a default
|
3
|
+
# e3db profile configuration so tests can execute
|
4
|
+
# against a live server.
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
# Check if the config is already set
|
9
|
+
if [ ! -d "$HOME/.tozny/integration-test" ]; then
|
10
|
+
mkdir -p "$HOME/.tozny/integration-test"
|
11
|
+
fi
|
12
|
+
|
13
|
+
cat > "$HOME/.tozny/integration-test/e3db.json" <<EOT
|
14
|
+
{
|
15
|
+
"version":1,
|
16
|
+
"api_url":"${API_URL}",
|
17
|
+
"api_key_id":"${API_KEY_ID}",
|
18
|
+
"api_secret":"${API_SECRET}",
|
19
|
+
"client_id":"${CLIENT_ID}",
|
20
|
+
"client_email":"${CLIENT_EMAIL}",
|
21
|
+
"public_key":"${PUBLIC_KEY}",
|
22
|
+
"private_key":"${PRIVATE_KEY}"
|
23
|
+
}
|
24
|
+
EOT
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# The purpose of this file is to install libsodium in
|
3
|
+
# the Travis CI environment. Outside this environment,
|
4
|
+
# you would probably not want to install it like this.
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
# check if libsodium is already installed
|
9
|
+
if [ ! -d "$HOME/libsodium/lib" ]; then
|
10
|
+
wget https://github.com/jedisct1/libsodium/releases/download/1.0.12/libsodium-1.0.12.tar.gz
|
11
|
+
tar xvfz libsodium-1.0.12.tar.gz
|
12
|
+
cd libsodium-1.0.12
|
13
|
+
./configure --prefix=$HOME/libsodium
|
14
|
+
make
|
15
|
+
make install
|
16
|
+
else
|
17
|
+
echo 'Using cached directory.'
|
18
|
+
fi
|
metadata
CHANGED
@@ -1,165 +1,179 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: e3db
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tozny, LLC
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.12'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.12'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '10.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - ~>
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '3.0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - ~>
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: simplecov
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - ~>
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: 0.14.1
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - ~>
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.14.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: coveralls
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.8.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.8.0
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: dry-struct
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
|
-
- - ~>
|
87
|
+
- - "~>"
|
74
88
|
- !ruby/object:Gem::Version
|
75
89
|
version: 0.2.1
|
76
90
|
type: :runtime
|
77
91
|
prerelease: false
|
78
92
|
version_requirements: !ruby/object:Gem::Requirement
|
79
93
|
requirements:
|
80
|
-
- - ~>
|
94
|
+
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: 0.2.1
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: lru_redux
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
86
100
|
requirements:
|
87
|
-
- - ~>
|
101
|
+
- - "~>"
|
88
102
|
- !ruby/object:Gem::Version
|
89
103
|
version: '1.1'
|
90
104
|
type: :runtime
|
91
105
|
prerelease: false
|
92
106
|
version_requirements: !ruby/object:Gem::Requirement
|
93
107
|
requirements:
|
94
|
-
- - ~>
|
108
|
+
- - "~>"
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '1.1'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: rbnacl
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
100
114
|
requirements:
|
101
|
-
- - ~>
|
115
|
+
- - "~>"
|
102
116
|
- !ruby/object:Gem::Version
|
103
117
|
version: '4.0'
|
104
|
-
- -
|
118
|
+
- - ">="
|
105
119
|
- !ruby/object:Gem::Version
|
106
120
|
version: 4.0.2
|
107
121
|
type: :runtime
|
108
122
|
prerelease: false
|
109
123
|
version_requirements: !ruby/object:Gem::Requirement
|
110
124
|
requirements:
|
111
|
-
- - ~>
|
125
|
+
- - "~>"
|
112
126
|
- !ruby/object:Gem::Version
|
113
127
|
version: '4.0'
|
114
|
-
- -
|
128
|
+
- - ">="
|
115
129
|
- !ruby/object:Gem::Version
|
116
130
|
version: 4.0.2
|
117
131
|
- !ruby/object:Gem::Dependency
|
118
132
|
name: net-http-persistent
|
119
133
|
requirement: !ruby/object:Gem::Requirement
|
120
134
|
requirements:
|
121
|
-
- - ~>
|
135
|
+
- - "~>"
|
122
136
|
- !ruby/object:Gem::Version
|
123
137
|
version: 2.9.4
|
124
138
|
type: :runtime
|
125
139
|
prerelease: false
|
126
140
|
version_requirements: !ruby/object:Gem::Requirement
|
127
141
|
requirements:
|
128
|
-
- - ~>
|
142
|
+
- - "~>"
|
129
143
|
- !ruby/object:Gem::Version
|
130
144
|
version: 2.9.4
|
131
145
|
- !ruby/object:Gem::Dependency
|
132
146
|
name: faraday_middleware
|
133
147
|
requirement: !ruby/object:Gem::Requirement
|
134
148
|
requirements:
|
135
|
-
- - ~>
|
149
|
+
- - "~>"
|
136
150
|
- !ruby/object:Gem::Version
|
137
151
|
version: 0.11.0.1
|
138
152
|
type: :runtime
|
139
153
|
prerelease: false
|
140
154
|
version_requirements: !ruby/object:Gem::Requirement
|
141
155
|
requirements:
|
142
|
-
- - ~>
|
156
|
+
- - "~>"
|
143
157
|
- !ruby/object:Gem::Version
|
144
158
|
version: 0.11.0.1
|
145
159
|
- !ruby/object:Gem::Dependency
|
146
160
|
name: oauth2
|
147
161
|
requirement: !ruby/object:Gem::Requirement
|
148
162
|
requirements:
|
149
|
-
- - ~>
|
163
|
+
- - "~>"
|
150
164
|
- !ruby/object:Gem::Version
|
151
165
|
version: '1.3'
|
152
|
-
- -
|
166
|
+
- - ">="
|
153
167
|
- !ruby/object:Gem::Version
|
154
168
|
version: 1.3.1
|
155
169
|
type: :runtime
|
156
170
|
prerelease: false
|
157
171
|
version_requirements: !ruby/object:Gem::Requirement
|
158
172
|
requirements:
|
159
|
-
- - ~>
|
173
|
+
- - "~>"
|
160
174
|
- !ruby/object:Gem::Version
|
161
175
|
version: '1.3'
|
162
|
-
- -
|
176
|
+
- - ">="
|
163
177
|
- !ruby/object:Gem::Version
|
164
178
|
version: 1.3.1
|
165
179
|
description:
|
@@ -169,9 +183,10 @@ executables: []
|
|
169
183
|
extensions: []
|
170
184
|
extra_rdoc_files: []
|
171
185
|
files:
|
172
|
-
- .gitignore
|
173
|
-
- .rspec
|
174
|
-
- .travis.yml
|
186
|
+
- ".gitignore"
|
187
|
+
- ".rspec"
|
188
|
+
- ".travis.yml"
|
189
|
+
- ".yardopts"
|
175
190
|
- Gemfile
|
176
191
|
- LICENSE.txt
|
177
192
|
- README.md
|
@@ -179,12 +194,15 @@ files:
|
|
179
194
|
- bin/console
|
180
195
|
- bin/setup
|
181
196
|
- e3db.gemspec
|
197
|
+
- examples/simple.rb
|
182
198
|
- lib/e3db.rb
|
183
199
|
- lib/e3db/client.rb
|
184
200
|
- lib/e3db/config.rb
|
185
201
|
- lib/e3db/crypto.rb
|
186
202
|
- lib/e3db/types.rb
|
187
203
|
- lib/e3db/version.rb
|
204
|
+
- travis-install-configfile.sh
|
205
|
+
- travis-install-libsodium.sh
|
188
206
|
homepage: https://tozny.com/e3db
|
189
207
|
licenses:
|
190
208
|
- MIT
|
@@ -195,17 +213,17 @@ require_paths:
|
|
195
213
|
- lib
|
196
214
|
required_ruby_version: !ruby/object:Gem::Requirement
|
197
215
|
requirements:
|
198
|
-
- -
|
216
|
+
- - ">="
|
199
217
|
- !ruby/object:Gem::Version
|
200
218
|
version: '0'
|
201
219
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
202
220
|
requirements:
|
203
|
-
- -
|
221
|
+
- - ">"
|
204
222
|
- !ruby/object:Gem::Version
|
205
|
-
version:
|
223
|
+
version: 1.3.1
|
206
224
|
requirements: []
|
207
225
|
rubyforge_project:
|
208
|
-
rubygems_version: 2.
|
226
|
+
rubygems_version: 2.6.8
|
209
227
|
signing_key:
|
210
228
|
specification_version: 4
|
211
229
|
summary: e3db client SDK
|