e3db 1.0.0 → 2.0.0.rc1
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 +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
|