parakey-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE +21 -0
- data/README.md +149 -0
- data/lib/parakey/access_events/normalize.rb +42 -0
- data/lib/parakey/access_grants/create.rb +47 -0
- data/lib/parakey/access_grants/delete.rb +19 -0
- data/lib/parakey/access_grants/index.rb +42 -0
- data/lib/parakey/access_grants.rb +13 -0
- data/lib/parakey/api_error.rb +20 -0
- data/lib/parakey/client.rb +49 -0
- data/lib/parakey/config.rb +16 -0
- data/lib/parakey/error.rb +6 -0
- data/lib/parakey/keys/index.rb +34 -0
- data/lib/parakey/locks/index.rb +60 -0
- data/lib/parakey/members/destroy.rb +23 -0
- data/lib/parakey/members/index.rb +17 -0
- data/lib/parakey/operation.rb +45 -0
- data/lib/parakey/version.rb +3 -0
- data/lib/parakey-client.rb +1 -0
- data/lib/parakey.rb +37 -0
- data/sig/parakey.rbs +107 -0
- metadata +136 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a7a9e0253c3a8e5a39e372c39c77be783d54ae0875290af41a2215588ed96b1c
|
|
4
|
+
data.tar.gz: 1cf12063617bcc57125e2560e9cc44b6e4906c06e93101be114ca1ea3fae3e24
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1539da5296f053cfaa0cf0bbf0ecb22ab12f4d95811a5a44f94b1e46af1b029ddc3d0807296f0d7b16d5078568b2380681bab3b6ecf7720a547ab8c98fd9e85a
|
|
7
|
+
data.tar.gz: e63ec011675cf935074272dee5f01f5e3ad5ffbb1be966ad5234f6d35d10827e4329fdf41fbe0f6fcc93f7a60c3a2ad3ae597243deffca50a815ef948874caaa
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-11-10
|
|
6
|
+
|
|
7
|
+
First release as `parakey-client`, extracted from the Parakey integration Guestit runs in production. The specs are
|
|
8
|
+
that integration's provider specs, ported with their recorded VCR cassettes (scrubbed of emails, names and unit
|
|
9
|
+
identifiers — this repo is public).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `Parakey.configure` for the access token, the domain id, the base URL and the request timeout. No Rails
|
|
14
|
+
dependency.
|
|
15
|
+
- Wire API on the `Parakey` module: `connection` and `request` with the retry policy (reads retry 429/502/503/504
|
|
16
|
+
and connection-level failures; a write never retries), the `Access-Token` auth header and the `ApiError` wrap with
|
|
17
|
+
`#unapplied?` delivery classification.
|
|
18
|
+
- `sig/parakey.rbs` declaring the public API.
|
|
19
|
+
- Operations, each returning a dry-monads `Result`:
|
|
20
|
+
- Access grants: `Create` (regular and time-controlled windows), `Delete` and `Index`, sharing the
|
|
21
|
+
`Parakey::AccessGrants` kind and status vocabulary.
|
|
22
|
+
- `Keys::Index` for the API-enabled accesses, `Locks::Index` for the devices joined with their accesses and
|
|
23
|
+
battery charge.
|
|
24
|
+
- Members: `Index` with wire filters and `Destroy` by email.
|
|
25
|
+
- `AccessEvents::Normalize` for event-log items and webhook deliveries.
|
|
26
|
+
|
|
27
|
+
[Unreleased]: https://github.com/guestitab/parakey-client/compare/v0.1.0...staging
|
|
28
|
+
[0.1.0]: https://github.com/guestitab/parakey-client/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guestit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# parakey-client
|
|
2
|
+
|
|
3
|
+
A Ruby client for the [Parakey API](https://api.parakey.co/). Each endpoint is one operation class that returns a
|
|
4
|
+
[dry-monads](https://dry-rb.org/gems/dry-monads/) `Result`. The gem handles authentication, retries and response
|
|
5
|
+
normalization. It does not depend on Rails, only on ActiveSupport and Faraday. Your app supplies the access token
|
|
6
|
+
and the domain id.
|
|
7
|
+
|
|
8
|
+
[Guestit](https://guestit.se) built it and runs it in production.
|
|
9
|
+
|
|
10
|
+
Requires Ruby 4.0 or later.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Gemfile
|
|
16
|
+
gem 'parakey-client'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Bundler loads it for you. Outside Bundler, `require 'parakey'`.
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
Parakey.configure do |config|
|
|
25
|
+
config.access_token = ENV.fetch('PARAKEY_ACCESS_TOKEN')
|
|
26
|
+
config.domain_id = ENV.fetch('PARAKEY_DOMAIN_ID')
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Setting | Default | What it does |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `access_token` | `nil` | The account's static API token, sent as the `Access-Token` header. Parakey issues one per account; there is no OAuth minting and no token store. |
|
|
33
|
+
| `domain_id` | `nil` | The domain every list and member call scopes to (the `domainId` parameter). |
|
|
34
|
+
| `base_url` | `https://api.parakey.co/` | API root. |
|
|
35
|
+
| `timeout` | `nil` | Faraday request timeout in seconds. |
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
Operations live under `Parakey::<Resource>::<Action>`. Build a fresh instance for every call:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
result = Parakey::AccessGrants::Index.new.call(skip: 0, take: 100)
|
|
43
|
+
|
|
44
|
+
if result.success?
|
|
45
|
+
result.value! # => [{ external_id: 'f163gXNFWc', recipient_email: '...', kind: 'regular', ... }]
|
|
46
|
+
else
|
|
47
|
+
result.failure # => a Parakey::Error or a Faraday::Error
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`call` never raises. It returns `Success(value)` or `Failure(error)`. `call!` returns the `Success` and raises the
|
|
52
|
+
error where `call` would return a `Failure`. Response bodies come back as hashes with symbol keys.
|
|
53
|
+
|
|
54
|
+
The gem covers access grants (create, delete, index), accesses (`Keys::Index`), devices (`Locks::Index`, joining
|
|
55
|
+
the accesses that reach each device), members (index, destroy) and access events (`AccessEvents::Normalize`, which
|
|
56
|
+
maps one event-log item or webhook delivery to plain attributes). [CHANGELOG.md](CHANGELOG.md) lists every
|
|
57
|
+
operation.
|
|
58
|
+
|
|
59
|
+
### Grants, keys and locks
|
|
60
|
+
|
|
61
|
+
An access key (a grant) points at an access (a reusable key) and a user. A key with both `startDate` and
|
|
62
|
+
`expirationDate` is time-controlled (`kind: 'tca'`); any other is permanent (`kind: 'regular'`). Creating a
|
|
63
|
+
time-controlled grant without the window, or a permanent one with one, is refused before any request:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Parakey::AccessGrants::Create.new.call(
|
|
67
|
+
attributes: {
|
|
68
|
+
access_id: 'hJ7Jt5aPIL',
|
|
69
|
+
recipient_email: 'guest@example.com',
|
|
70
|
+
kind: 'tca',
|
|
71
|
+
start_date: Time.utc(2026, 11, 10),
|
|
72
|
+
end_date: Time.utc(2026, 11, 12)
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Parakey upserts an access key by access, user and window: re-creating an identical grant resolves to the existing
|
|
78
|
+
key. Deletes answer 404 for an already-missing key or member, which the gem reads as success.
|
|
79
|
+
|
|
80
|
+
Lists page 1-based with `page` and `limit` on the wire; the operations take `skip` and `take` and derive the page.
|
|
81
|
+
`Keys::Index` is always filtered to API-enabled accesses; `Locks::Index` reports `batteryStatus` as a percentage
|
|
82
|
+
(`high`/`medium`/`low`/`veryLow` → 100/75/50/25).
|
|
83
|
+
|
|
84
|
+
### Errors
|
|
85
|
+
|
|
86
|
+
| Class | Raised when |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `Parakey::ArgumentError` | The arguments are unusable. Raised before any request. |
|
|
89
|
+
| `Parakey::ApiError` | Parakey answered with an error status. `#status` holds the HTTP status. |
|
|
90
|
+
| `Parakey::Error` | The parent of the two above, also raised when no access token is configured. |
|
|
91
|
+
|
|
92
|
+
### Retries and delivery
|
|
93
|
+
|
|
94
|
+
Reads retry 429/502/503/504 and connection-level failures with backoff. A write never retries on a status, and a
|
|
95
|
+
connection-level failure retries on reads only — `POST /access-keys` has no provider idempotency key. After a
|
|
96
|
+
failed write:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
error.is_a?(Parakey::ApiError) && error.unapplied? # the write certainly did not take, so a retry is safe
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Anything else (a server error, a timeout after the request left) leaves delivery unknown: read back before
|
|
103
|
+
retrying.
|
|
104
|
+
|
|
105
|
+
### Webhooks
|
|
106
|
+
|
|
107
|
+
Parakey POSTs event-log items to your webhook URL with the account's webhook key in the `?token=` query parameter.
|
|
108
|
+
Verify it in your app (a `secure_compare` against your stored key), then normalize the payload:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
Parakey::AccessEvents::Normalize.new.call(item: params)
|
|
112
|
+
# => Success({ external_id:, event_type:, occurred_at:, grant_external_id:, provider_lock_id:, access_id:, metadata: })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A delivery gets six attempts over about five hours and is then dropped, and Parakey disables a webhook after 10
|
|
116
|
+
failures in five days or 100 in a day. Dropped events can be recovered from `GET /event-logs`
|
|
117
|
+
(`Accept-Version: 2.0`), polling by `createdAt`.
|
|
118
|
+
|
|
119
|
+
## Type signatures
|
|
120
|
+
|
|
121
|
+
The gem ships RBS for its public API in `sig/parakey.rbs`. `rbs collection install` picks it up from your
|
|
122
|
+
`Gemfile.lock`, so Steep sees it with no extra setup.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
bundle install
|
|
128
|
+
bundle exec rake # specs and RuboCop
|
|
129
|
+
bundle exec rspec # specs only
|
|
130
|
+
bundle exec rubocop -A # autocorrect; review every change
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The specs run offline against recorded VCR cassettes in `spec/fixtures/cassettes`. Recording a new cassette needs
|
|
134
|
+
live credentials for a Parakey account. Scrub the token, the domain id (the spec_helper filters handle both) and
|
|
135
|
+
any real emails, names or unit identifiers from the cassette before you commit it — this repo is public.
|
|
136
|
+
|
|
137
|
+
## Releasing
|
|
138
|
+
|
|
139
|
+
1. Bump `Parakey::VERSION` in `lib/parakey/version.rb` and move the `Unreleased` notes in `CHANGELOG.md` under the
|
|
140
|
+
new version.
|
|
141
|
+
2. Commit on `staging` and run `bundle exec rake`.
|
|
142
|
+
3. Run `gem signin` once per machine. The gem requires MFA, so every push asks for a one-time code.
|
|
143
|
+
4. Run `bundle exec rake release`. It builds `pkg/parakey-client-<version>.gem`, tags `v<version>`, pushes the
|
|
144
|
+
branch and the tag, and pushes the gem to RubyGems.
|
|
145
|
+
5. Fast-forward `production` to the release tag.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
module AccessEvents
|
|
3
|
+
# Normalizes one event-log item or webhook delivery into the access event
|
|
4
|
+
# attributes the host persists. The wire item is left untouched under
|
|
5
|
+
# metadata[:payload].
|
|
6
|
+
class Normalize
|
|
7
|
+
include Parakey::Operation
|
|
8
|
+
|
|
9
|
+
# Device maintenance activities report as a sync, not an unlock.
|
|
10
|
+
SYNC_ACTIVITIES = %w[calibrate dfu setTime].freeze #: ::Array[::String]
|
|
11
|
+
|
|
12
|
+
# @rbs (item: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
13
|
+
def call(item:)
|
|
14
|
+
argument! item: :required if item.blank?
|
|
15
|
+
|
|
16
|
+
Success(attributes(item))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# @rbs (::Hash[::Symbol, untyped] item) -> ::Hash[::Symbol, untyped]
|
|
22
|
+
def attributes(item)
|
|
23
|
+
{
|
|
24
|
+
external_id: item[:id],
|
|
25
|
+
event_type: event_type(item),
|
|
26
|
+
occurred_at: item[:timestamp].presence&.in_time_zone,
|
|
27
|
+
grant_external_id: item.dig(:info, :accessKey, :id).presence,
|
|
28
|
+
provider_lock_id: item.dig(:info, :device, :id).presence,
|
|
29
|
+
access_id: item.dig(:info, :access, :id).presence,
|
|
30
|
+
metadata: { payload: item }
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs (::Hash[::Symbol, untyped] item) -> ::String?
|
|
35
|
+
def event_type(item)
|
|
36
|
+
return 'synced' if SYNC_ACTIVITIES.include?(item[:activity])
|
|
37
|
+
|
|
38
|
+
item[:activity].presence
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
module AccessGrants
|
|
3
|
+
# Creates an access key: permanent for a regular grant, time-windowed for a
|
|
4
|
+
# time-controlled one. Parakey upserts by access, user and window, so a repeat
|
|
5
|
+
# of an identical grant resolves to the existing key instead of duplicating it.
|
|
6
|
+
class Create
|
|
7
|
+
include Parakey::Operation
|
|
8
|
+
|
|
9
|
+
# @rbs (attributes: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
10
|
+
def call(attributes:)
|
|
11
|
+
argument! access_id: :required if attributes[:access_id].blank?
|
|
12
|
+
argument! kind: :invalid unless KINDS.include?(attributes[:kind])
|
|
13
|
+
argument! recipient_email: :required if attributes[:recipient_email].blank?
|
|
14
|
+
|
|
15
|
+
validate_validity_window!(attributes)
|
|
16
|
+
|
|
17
|
+
body = Parakey.request(:post, 'access-keys', params: payload(attributes))
|
|
18
|
+
|
|
19
|
+
Success(external_id: body.fetch(:id), metadata: { raw: body })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# A regular grant never carries a window; a time-controlled one always
|
|
25
|
+
# does, ending after it starts.
|
|
26
|
+
# @rbs (::Hash[::Symbol, untyped] attributes) -> void
|
|
27
|
+
def validate_validity_window!(attributes)
|
|
28
|
+
if attributes[:kind] == KIND_TIME_CONTROLLED
|
|
29
|
+
argument! validity_window: :required if attributes[:start_date].blank? || attributes[:end_date].blank?
|
|
30
|
+
argument! end_date: :invalid if attributes[:start_date].in_time_zone >= attributes[:end_date].in_time_zone
|
|
31
|
+
elsif attributes[:start_date].present? || attributes[:end_date].present?
|
|
32
|
+
argument! validity_window: :invalid
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @rbs (::Hash[::Symbol, untyped] attributes) -> ::Hash[::Symbol, untyped]
|
|
37
|
+
def payload(attributes)
|
|
38
|
+
{
|
|
39
|
+
access: { id: attributes[:access_id] },
|
|
40
|
+
user: { email: attributes[:recipient_email] },
|
|
41
|
+
startDate: attributes[:start_date]&.iso8601,
|
|
42
|
+
expirationDate: attributes[:end_date]&.iso8601
|
|
43
|
+
}.compact
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
module AccessGrants
|
|
3
|
+
# Deletes an access key. A key that is already gone answers 404, which reads
|
|
4
|
+
# as success: the desired end state is reached either way.
|
|
5
|
+
class Delete
|
|
6
|
+
include Parakey::Operation
|
|
7
|
+
|
|
8
|
+
# @rbs (attributes: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[bool]
|
|
9
|
+
def call(attributes:)
|
|
10
|
+
argument! external_id: :required if attributes[:external_id].blank?
|
|
11
|
+
|
|
12
|
+
path = "access-keys/#{ERB::Util.url_encode(attributes[:external_id])}"
|
|
13
|
+
Parakey.request(:delete, path, accepted_statuses: [404])
|
|
14
|
+
|
|
15
|
+
Success(true)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
module AccessGrants
|
|
3
|
+
# Lists the domain's access keys, one page per call.
|
|
4
|
+
class Index
|
|
5
|
+
include Parakey::Operation
|
|
6
|
+
|
|
7
|
+
# @rbs (?skip: ::Integer, ?take: ::Integer) -> Dry::Monads::Result[::Array[::Hash[::Symbol, untyped]]]
|
|
8
|
+
def call(skip: 0, take: 100)
|
|
9
|
+
body = Parakey.request(:get, 'access-keys', params: page_params(skip, take))
|
|
10
|
+
|
|
11
|
+
Success(body.fetch(:items, []).map { |item| attributes(item) })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# @rbs (::Hash[::Symbol, untyped] item) -> ::Hash[::Symbol, untyped]
|
|
17
|
+
def attributes(item)
|
|
18
|
+
user = item[:user] || {}
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
external_id: item[:id],
|
|
22
|
+
label: item[:aliasTag],
|
|
23
|
+
recipient_email: user[:email],
|
|
24
|
+
recipient_name: user[:name],
|
|
25
|
+
status: item[:enabled] ? STATUS_ACTIVE : STATUS_INACTIVE,
|
|
26
|
+
kind: grant_kind(item),
|
|
27
|
+
access_id: item.dig(:access, :id),
|
|
28
|
+
start_date: item[:startDate],
|
|
29
|
+
end_date: item[:expirationDate]
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A key with both dates is time-controlled; anything else is permanent.
|
|
34
|
+
# @rbs (::Hash[::Symbol, untyped] item) -> ::String
|
|
35
|
+
def grant_kind(item)
|
|
36
|
+
return KIND_TIME_CONTROLLED if item[:startDate].present? && item[:expirationDate].present?
|
|
37
|
+
|
|
38
|
+
KIND_REGULAR
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
# The normalized vocabulary the access-key operations return. Parakey access
|
|
3
|
+
# keys come in two kinds: a permanent key (regular) and a time-windowed key
|
|
4
|
+
# (time-controlled access, tca); a key is active while enabled.
|
|
5
|
+
module AccessGrants
|
|
6
|
+
KIND_REGULAR = 'regular'.freeze
|
|
7
|
+
KIND_TIME_CONTROLLED = 'tca'.freeze
|
|
8
|
+
KINDS = [KIND_REGULAR, KIND_TIME_CONTROLLED].freeze #: ::Array[::String]
|
|
9
|
+
|
|
10
|
+
STATUS_ACTIVE = 'active'.freeze
|
|
11
|
+
STATUS_INACTIVE = 'inactive'.freeze
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
# A rejected Parakey response, carrying the HTTP status. #unapplied? marks a
|
|
3
|
+
# refusal where the write certainly did not land, so a retry is safe; anything
|
|
4
|
+
# else (a server error, a timeout after a write) leaves delivery unknown.
|
|
5
|
+
class ApiError < Error
|
|
6
|
+
attr_reader :status #: ::Integer
|
|
7
|
+
|
|
8
|
+
# @rbs (::String message, status: ::Integer) -> void
|
|
9
|
+
def initialize(message, status:)
|
|
10
|
+
super(message)
|
|
11
|
+
|
|
12
|
+
@status = status
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @rbs () -> bool
|
|
16
|
+
def unapplied?
|
|
17
|
+
status.between?(400, 499) && [408, 409].exclude?(status)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
BASE_URL = 'https://api.parakey.co/'.freeze #: ::String
|
|
3
|
+
|
|
4
|
+
# Access-key POSTs create grants without a provider idempotency key, so a write
|
|
5
|
+
# never retries; reads retry on a status or a connection-level failure.
|
|
6
|
+
RETRY_OPTIONS = {
|
|
7
|
+
max: 3,
|
|
8
|
+
interval: 0.5,
|
|
9
|
+
interval_randomness: 0.5,
|
|
10
|
+
backoff_factor: 2,
|
|
11
|
+
retry_statuses: [429, 502, 503, 504],
|
|
12
|
+
methods: %i[get delete],
|
|
13
|
+
exceptions: [*Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS, Faraday::ConnectionFailed, Faraday::SSLError]
|
|
14
|
+
}.freeze #: ::Hash[::Symbol, untyped]
|
|
15
|
+
|
|
16
|
+
# @rbs () -> Faraday::Connection
|
|
17
|
+
def self.connection
|
|
18
|
+
@connection ||= Faraday.new(url: config.base_url) do |builder|
|
|
19
|
+
builder.request :json
|
|
20
|
+
builder.request :retry, RETRY_OPTIONS
|
|
21
|
+
builder.response :json, parser_options: { symbolize_names: true, mode: :rails, decoder: [Oj, :load] }
|
|
22
|
+
builder.options[:timeout] = config.timeout
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @rbs (::Symbol method, ::String path, ?params: ::Hash[::Symbol, untyped]?, ?accepted_statuses: ::Array[::Integer]) -> untyped
|
|
27
|
+
def self.request(method, path, params: nil, accepted_statuses: [])
|
|
28
|
+
response = connection.public_send(method, path, params, auth_headers)
|
|
29
|
+
|
|
30
|
+
return response.body if response.success? || accepted_statuses.include?(response.status)
|
|
31
|
+
|
|
32
|
+
raise ApiError.new(error_message(response), status: response.status)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs () -> ::Hash[::String, ::String]
|
|
36
|
+
private_class_method def self.auth_headers
|
|
37
|
+
access_token = config.access_token.presence || raise(Error, 'Parakey access token is required.')
|
|
38
|
+
|
|
39
|
+
{ 'Access-Token' => access_token }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs (Faraday::Response response) -> ::String
|
|
43
|
+
private_class_method def self.error_message(response)
|
|
44
|
+
body = response.body
|
|
45
|
+
message = body.is_a?(Hash) ? body[:message] : nil
|
|
46
|
+
|
|
47
|
+
message.presence || body.to_s.presence || "Provider request failed with status #{response.status}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
# Injected by the host app (see the Parakey.configure initializer): the static
|
|
3
|
+
# API access token and the domain every call scopes to. Keeping these out of
|
|
4
|
+
# the gem is what makes it host-agnostic.
|
|
5
|
+
#
|
|
6
|
+
# Parakey issues one long-lived access token per account (no OAuth minting), so
|
|
7
|
+
# there is no token store to inject; the host reads the token from its own
|
|
8
|
+
# credentials and hands it over here.
|
|
9
|
+
class Config
|
|
10
|
+
attr_accessor :access_token, :domain_id, :base_url, :timeout
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@base_url = BASE_URL
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
module Keys
|
|
3
|
+
# Lists the domain's accesses (the reusable "keys" a grant points at),
|
|
4
|
+
# restricted to API-enabled ones. Extra wire filters merge in verbatim, e.g.
|
|
5
|
+
# { deviceIds: { in: [...] } }.
|
|
6
|
+
class Index
|
|
7
|
+
include Parakey::Operation
|
|
8
|
+
|
|
9
|
+
ACCESS_FILTERS = {
|
|
10
|
+
allowedOrigins: 'api',
|
|
11
|
+
enabled: true
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
# @rbs (?skip: ::Integer, ?take: ::Integer, ?filters: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::Array[::Hash[::Symbol, untyped]]]
|
|
15
|
+
def call(skip: 0, take: 100, filters: {})
|
|
16
|
+
params = ACCESS_FILTERS.merge(filters, page_params(skip, take))
|
|
17
|
+
body = Parakey.request(:get, 'accesses', params:)
|
|
18
|
+
|
|
19
|
+
Success(body.fetch(:items, []).map { |item| attributes(item) })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# @rbs (::Hash[::Symbol, untyped] item) -> ::Hash[::Symbol, untyped]
|
|
25
|
+
def attributes(item)
|
|
26
|
+
{
|
|
27
|
+
external_id: item[:id],
|
|
28
|
+
name: item[:name].presence,
|
|
29
|
+
device_ids: item.fetch(:devices, []).pluck(:id)
|
|
30
|
+
}.compact
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
module Locks
|
|
3
|
+
# Lists the domain's devices, each with the accesses that reach it. One page
|
|
4
|
+
# of devices per call; the accesses join pages through Keys::Index on its own.
|
|
5
|
+
class Index
|
|
6
|
+
include Parakey::Operation
|
|
7
|
+
|
|
8
|
+
BATTERY_CHARGES = {
|
|
9
|
+
'high' => 100,
|
|
10
|
+
'medium' => 75,
|
|
11
|
+
'low' => 50,
|
|
12
|
+
'veryLow' => 25
|
|
13
|
+
}.freeze #: ::Hash[::String, ::Integer]
|
|
14
|
+
|
|
15
|
+
ACCESS_BATCH_SIZE = 500
|
|
16
|
+
|
|
17
|
+
# The wire reports no device status; a device the API lists reads as online.
|
|
18
|
+
STATUS_ONLINE = 'online'.freeze
|
|
19
|
+
|
|
20
|
+
# @rbs (?skip: ::Integer, ?take: ::Integer) -> Dry::Monads::Result[::Array[::Hash[::Symbol, untyped]]]
|
|
21
|
+
def call(skip: 0, take: 100)
|
|
22
|
+
devices = Parakey.request(:get, 'devices', params: page_params(skip, take)).fetch(:items, [])
|
|
23
|
+
accesses = accesses(devices.pluck(:id))
|
|
24
|
+
|
|
25
|
+
Success(devices.map { |device| attributes(device, accesses) })
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# @rbs (::Array[::String] device_ids) -> ::Array[::Hash[::Symbol, untyped]]
|
|
31
|
+
def accesses(device_ids)
|
|
32
|
+
return [] if device_ids.empty?
|
|
33
|
+
|
|
34
|
+
filters = { deviceIds: { in: device_ids } }
|
|
35
|
+
result = []
|
|
36
|
+
|
|
37
|
+
loop do
|
|
38
|
+
page = Parakey::Keys::Index.new.call!(skip: result.size, take: ACCESS_BATCH_SIZE, filters:).value!
|
|
39
|
+
result.concat(page)
|
|
40
|
+
break if page.size < ACCESS_BATCH_SIZE
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @rbs (::Hash[::Symbol, untyped] device, ::Array[::Hash[::Symbol, untyped]] accesses) -> ::Hash[::Symbol, untyped]
|
|
47
|
+
def attributes(device, accesses)
|
|
48
|
+
reaching = accesses.select { |access| access[:device_ids].include?(device[:id]) }
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
external_id: device[:id],
|
|
52
|
+
name: device[:name],
|
|
53
|
+
status: STATUS_ONLINE,
|
|
54
|
+
charge: BATTERY_CHARGES[device[:batteryStatus]],
|
|
55
|
+
keys: reaching.map { |access| access.slice(:external_id, :name) }
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
module Members
|
|
3
|
+
# Removes a domain member by email. An unknown member answers 404, which
|
|
4
|
+
# reads as success: the member is gone either way.
|
|
5
|
+
class Destroy
|
|
6
|
+
include Parakey::Operation
|
|
7
|
+
|
|
8
|
+
# @rbs (email: ::String?) -> Dry::Monads::Result[bool]
|
|
9
|
+
def call(email:)
|
|
10
|
+
argument! email: :required if email.blank?
|
|
11
|
+
argument! domain_id: :required if Parakey.config.domain_id.blank?
|
|
12
|
+
|
|
13
|
+
filter = {
|
|
14
|
+
domainId: Parakey.config.domain_id,
|
|
15
|
+
email:
|
|
16
|
+
}
|
|
17
|
+
Parakey.request(:post, 'members/delete', params: { filter: }, accepted_statuses: [404])
|
|
18
|
+
|
|
19
|
+
Success(true)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
module Members
|
|
3
|
+
# Lists the domain's members, one page per call, items in the wire's own
|
|
4
|
+
# shape (symbolized camelCase keys). Wire filters merge in verbatim, e.g.
|
|
5
|
+
# { status: 'emailProblem' } for members whose invitation email bounced.
|
|
6
|
+
class Index
|
|
7
|
+
include Parakey::Operation
|
|
8
|
+
|
|
9
|
+
# @rbs (?skip: ::Integer, ?take: ::Integer, ?filters: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::Array[::Hash[::Symbol, untyped]]]
|
|
10
|
+
def call(skip: 0, take: 100, filters: {})
|
|
11
|
+
body = Parakey.request(:get, 'members', params: filters.merge(page_params(skip, take)))
|
|
12
|
+
|
|
13
|
+
Success(body.fetch(:items, []))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Parakey
|
|
2
|
+
# Shared monadic contract for the client operations: #call returns a Result,
|
|
3
|
+
# #call! unwraps it and raises the underlying failure. An error raised inside
|
|
4
|
+
# #call becomes Failure(error), mirroring the host's operation base.
|
|
5
|
+
module Operation
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.include Dry::Monads[:result]
|
|
8
|
+
base.prepend(Call)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Call
|
|
12
|
+
def call(*, **, &)
|
|
13
|
+
super
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
Failure(e)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call!(...)
|
|
20
|
+
result = call(...)
|
|
21
|
+
raise result.failure if result.failure?
|
|
22
|
+
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def argument!(**attrs)
|
|
29
|
+
key, reason = attrs.first
|
|
30
|
+
raise Parakey::ArgumentError, "#{key}: #{reason}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Lists page 1-based by page and limit on the wire; the operations take an item offset.
|
|
34
|
+
# @rbs (::Integer skip, ::Integer take) -> ::Hash[::Symbol, untyped]
|
|
35
|
+
def page_params(skip, take)
|
|
36
|
+
argument! take: :invalid unless take.to_i.positive?
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
domainId: Parakey.config.domain_id,
|
|
40
|
+
limit: take,
|
|
41
|
+
page: skip.div(take) + 1
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative 'parakey'
|
data/lib/parakey.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'active_support'
|
|
2
|
+
require 'active_support/core_ext'
|
|
3
|
+
require 'dry/monads'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require 'faraday'
|
|
6
|
+
require 'faraday/retry'
|
|
7
|
+
require 'oj'
|
|
8
|
+
|
|
9
|
+
require_relative 'parakey/version'
|
|
10
|
+
require_relative 'parakey/error'
|
|
11
|
+
require_relative 'parakey/api_error'
|
|
12
|
+
require_relative 'parakey/config'
|
|
13
|
+
require_relative 'parakey/operation'
|
|
14
|
+
require_relative 'parakey/client'
|
|
15
|
+
require_relative 'parakey/access_grants'
|
|
16
|
+
require_relative 'parakey/access_events/normalize'
|
|
17
|
+
require_relative 'parakey/access_grants/create'
|
|
18
|
+
require_relative 'parakey/access_grants/delete'
|
|
19
|
+
require_relative 'parakey/access_grants/index'
|
|
20
|
+
require_relative 'parakey/keys/index'
|
|
21
|
+
require_relative 'parakey/locks/index'
|
|
22
|
+
require_relative 'parakey/members/destroy'
|
|
23
|
+
require_relative 'parakey/members/index'
|
|
24
|
+
|
|
25
|
+
module Parakey
|
|
26
|
+
class << self
|
|
27
|
+
# @rbs () { (Config) -> void } -> void
|
|
28
|
+
def configure
|
|
29
|
+
yield(config)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @rbs () -> Config
|
|
33
|
+
def config
|
|
34
|
+
@config ||= Config.new
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/sig/parakey.rbs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Public API of the gem, mirroring the inline `# @rbs` annotations in lib/.
|
|
2
|
+
# Shipped with the gem so host apps pick it up through `rbs collection`.
|
|
3
|
+
module Parakey
|
|
4
|
+
VERSION: ::String
|
|
5
|
+
|
|
6
|
+
BASE_URL: ::String
|
|
7
|
+
RETRY_OPTIONS: ::Hash[::Symbol, untyped]
|
|
8
|
+
|
|
9
|
+
def self.configure: () { (Config) -> void } -> void
|
|
10
|
+
def self.config: () -> Config
|
|
11
|
+
|
|
12
|
+
def self.connection: () -> untyped
|
|
13
|
+
def self.request: (::Symbol method, ::String path, ?params: ::Hash[::Symbol, untyped]?, ?accepted_statuses: ::Array[::Integer]) -> untyped
|
|
14
|
+
|
|
15
|
+
class Error < ::StandardError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Raised by operations before any network call when the arguments are unusable.
|
|
19
|
+
class ArgumentError < Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A rejected Parakey response; #unapplied? marks a write that never landed.
|
|
23
|
+
class ApiError < Error
|
|
24
|
+
attr_reader status: ::Integer
|
|
25
|
+
|
|
26
|
+
def initialize: (::String message, status: ::Integer) -> void
|
|
27
|
+
|
|
28
|
+
def unapplied?: () -> bool
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Config
|
|
32
|
+
attr_accessor access_token: ::String?
|
|
33
|
+
attr_accessor domain_id: ::String?
|
|
34
|
+
attr_accessor base_url: ::String
|
|
35
|
+
attr_accessor timeout: (::Integer | ::Float)?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# #call! returns the same Result as #call, raising the failure instead of
|
|
39
|
+
# wrapping it — hence the `alias call! call` on every operation below.
|
|
40
|
+
module Operation
|
|
41
|
+
def call!: (*untyped, **untyped) -> untyped
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module AccessGrants
|
|
45
|
+
KIND_REGULAR: ::String
|
|
46
|
+
KIND_TIME_CONTROLLED: ::String
|
|
47
|
+
KINDS: ::Array[::String]
|
|
48
|
+
STATUS_ACTIVE: ::String
|
|
49
|
+
STATUS_INACTIVE: ::String
|
|
50
|
+
|
|
51
|
+
class Create
|
|
52
|
+
def call: (attributes: ::Hash[::Symbol, untyped]) -> ::Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
53
|
+
alias call! call
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Delete
|
|
57
|
+
def call: (attributes: ::Hash[::Symbol, untyped]) -> ::Dry::Monads::Result[bool]
|
|
58
|
+
alias call! call
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Index
|
|
62
|
+
def call: (?skip: ::Integer, ?take: ::Integer) -> ::Dry::Monads::Result[::Array[::Hash[::Symbol, untyped]]]
|
|
63
|
+
alias call! call
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
module AccessEvents
|
|
68
|
+
class Normalize
|
|
69
|
+
SYNC_ACTIVITIES: ::Array[::String]
|
|
70
|
+
|
|
71
|
+
def call: (item: ::Hash[::Symbol, untyped]) -> ::Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
72
|
+
alias call! call
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
module Keys
|
|
77
|
+
class Index
|
|
78
|
+
ACCESS_FILTERS: ::Hash[::Symbol, untyped]
|
|
79
|
+
|
|
80
|
+
def call: (?skip: ::Integer, ?take: ::Integer, ?filters: ::Hash[::Symbol, untyped]) -> ::Dry::Monads::Result[::Array[::Hash[::Symbol, untyped]]]
|
|
81
|
+
alias call! call
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
module Locks
|
|
86
|
+
class Index
|
|
87
|
+
BATTERY_CHARGES: ::Hash[::String, ::Integer]
|
|
88
|
+
ACCESS_BATCH_SIZE: ::Integer
|
|
89
|
+
STATUS_ONLINE: ::String
|
|
90
|
+
|
|
91
|
+
def call: (?skip: ::Integer, ?take: ::Integer) -> ::Dry::Monads::Result[::Array[::Hash[::Symbol, untyped]]]
|
|
92
|
+
alias call! call
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module Members
|
|
97
|
+
class Index
|
|
98
|
+
def call: (?skip: ::Integer, ?take: ::Integer, ?filters: ::Hash[::Symbol, untyped]) -> ::Dry::Monads::Result[::Array[::Hash[::Symbol, untyped]]]
|
|
99
|
+
alias call! call
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class Destroy
|
|
103
|
+
def call: (email: ::String?) -> ::Dry::Monads::Result[bool]
|
|
104
|
+
alias call! call
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: parakey-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Guestit
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dry-monads
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.10'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.10'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: faraday
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.14'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.14'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: faraday-retry
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.4'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.4'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: oj
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.17'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.17'
|
|
82
|
+
description: 'One operation per Parakey API endpoint, returning dry-monads Results,
|
|
83
|
+
with retries and response normalization. Framework-free: the host injects the access
|
|
84
|
+
token and the domain id.'
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files: []
|
|
88
|
+
files:
|
|
89
|
+
- CHANGELOG.md
|
|
90
|
+
- LICENSE
|
|
91
|
+
- README.md
|
|
92
|
+
- lib/parakey-client.rb
|
|
93
|
+
- lib/parakey.rb
|
|
94
|
+
- lib/parakey/access_events/normalize.rb
|
|
95
|
+
- lib/parakey/access_grants.rb
|
|
96
|
+
- lib/parakey/access_grants/create.rb
|
|
97
|
+
- lib/parakey/access_grants/delete.rb
|
|
98
|
+
- lib/parakey/access_grants/index.rb
|
|
99
|
+
- lib/parakey/api_error.rb
|
|
100
|
+
- lib/parakey/client.rb
|
|
101
|
+
- lib/parakey/config.rb
|
|
102
|
+
- lib/parakey/error.rb
|
|
103
|
+
- lib/parakey/keys/index.rb
|
|
104
|
+
- lib/parakey/locks/index.rb
|
|
105
|
+
- lib/parakey/members/destroy.rb
|
|
106
|
+
- lib/parakey/members/index.rb
|
|
107
|
+
- lib/parakey/operation.rb
|
|
108
|
+
- lib/parakey/version.rb
|
|
109
|
+
- sig/parakey.rbs
|
|
110
|
+
homepage: https://github.com/guestitab/parakey-client
|
|
111
|
+
licenses:
|
|
112
|
+
- MIT
|
|
113
|
+
metadata:
|
|
114
|
+
allowed_push_host: https://rubygems.org
|
|
115
|
+
source_code_uri: https://github.com/guestitab/parakey-client
|
|
116
|
+
changelog_uri: https://github.com/guestitab/parakey-client/blob/staging/CHANGELOG.md
|
|
117
|
+
bug_tracker_uri: https://github.com/guestitab/parakey-client/issues
|
|
118
|
+
rubygems_mfa_required: 'true'
|
|
119
|
+
rdoc_options: []
|
|
120
|
+
require_paths:
|
|
121
|
+
- lib
|
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '4.0'
|
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
requirements: []
|
|
133
|
+
rubygems_version: 4.0.23
|
|
134
|
+
specification_version: 4
|
|
135
|
+
summary: Ruby client for the Parakey API
|
|
136
|
+
test_files: []
|