doorflow 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/doorflow/client/credentials_proxy.rb +6 -9
- data/lib/doorflow/client/element_sessions_proxy.rb +91 -0
- data/lib/doorflow/client.rb +13 -0
- data/lib/doorflow/errors/doorflow_error.rb +1 -1
- data/lib/doorflow/resources/credential.rb +4 -3
- data/lib/doorflow/resources/element_session.rb +28 -0
- data/lib/doorflow/webhooks/handler.rb +6 -6
- data/lib/doorflow/webhooks/signature_verifier.rb +8 -5
- data/lib/doorflow-api/version.rb +1 -1
- data/lib/doorflow.rb +1 -0
- data/spec/doorflow/webhooks/event_spec.rb +117 -0
- data/spec/doorflow/webhooks/handler_spec.rb +154 -0
- data/spec/doorflow/webhooks/signature_verifier_spec.rb +210 -0
- data/spec/doorflow/webhooks/support.rb +18 -0
- data/spec/resources/credential_bugs_spec.rb +115 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 958e972d5b704e9dc612689508b92b2f889cd658db9987e12e073017a70a6942
|
|
4
|
+
data.tar.gz: c6aaa00b0644899890276825dbaca7022ee22ab92a1a70f35fb901ea9f07291f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ab31e49a003ed6217abad77bb8084ec214f3f3bb75ab2fbbd2f508f67ebaeddfc37933049fc4baa62680ad3f8fd54f90b2343bc82952aa12ac434b343ad8331
|
|
7
|
+
data.tar.gz: 22a30992a4455c7743d63fda56169e9867b9c339be4abd190d8ebda48d6624d542d2191c63c6c41196474e5768629d93bf1669b96a80a6f5bb2986e61a06f0c1
|
data/README.md
CHANGED
|
@@ -285,8 +285,8 @@ class WebhooksController < ApplicationController
|
|
|
285
285
|
def create
|
|
286
286
|
handler.handle(
|
|
287
287
|
payload: request.raw_post,
|
|
288
|
-
signature: request.headers['
|
|
289
|
-
timestamp: request.headers['
|
|
288
|
+
signature: request.headers['Signature'],
|
|
289
|
+
timestamp: request.headers['Timestamp']
|
|
290
290
|
)
|
|
291
291
|
head :ok
|
|
292
292
|
rescue DoorFlow::Webhooks::SignatureError
|
|
@@ -35,10 +35,11 @@ module DoorFlow
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Retrieve a credential by ID
|
|
38
|
+
# @param person_id [Integer] Person ID (required by the API)
|
|
38
39
|
# @param id [String, Integer] Credential ID
|
|
39
40
|
# @return [DoorFlow::Resources::Credential]
|
|
40
|
-
def retrieve(id)
|
|
41
|
-
response = client.credentials_api.get_credential(id)
|
|
41
|
+
def retrieve(person_id, id)
|
|
42
|
+
response = client.credentials_api.get_credential(person_id, id)
|
|
42
43
|
to_resource(response, Resources::Credential)
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -56,14 +57,10 @@ module DoorFlow
|
|
|
56
57
|
|
|
57
58
|
# Delete a credential
|
|
58
59
|
# @param id [String, Integer] Credential ID
|
|
59
|
-
# @param person_id [
|
|
60
|
+
# @param person_id [Integer] Person ID (required by the API)
|
|
60
61
|
# @return [Boolean] true if successful
|
|
61
|
-
def delete(id, person_id:
|
|
62
|
-
|
|
63
|
-
client.credentials_api.delete_credential(person_id, id)
|
|
64
|
-
else
|
|
65
|
-
client.credentials_api.delete_credential(id)
|
|
66
|
-
end
|
|
62
|
+
def delete(id, person_id:)
|
|
63
|
+
client.credentials_api.delete_credential(person_id, id)
|
|
67
64
|
true
|
|
68
65
|
rescue => e
|
|
69
66
|
false
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module DoorFlow
|
|
2
|
+
class Client
|
|
3
|
+
# Base proxy for Element Sessions — handles the HTTP call
|
|
4
|
+
#
|
|
5
|
+
# @api private
|
|
6
|
+
class ElementSessionsProxy < ResourceProxy
|
|
7
|
+
def create_session(widget_type:, member:, credentials: nil)
|
|
8
|
+
params = {
|
|
9
|
+
widget_type: widget_type,
|
|
10
|
+
member: {
|
|
11
|
+
external_id: member[:external_id],
|
|
12
|
+
name: member[:name],
|
|
13
|
+
email: member[:email]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
params[:credentials] = credentials if credentials
|
|
17
|
+
response = post("/api/3/element_sessions", params)
|
|
18
|
+
to_resource(response, Resources::ElementSession)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def post(path, params)
|
|
24
|
+
api_client = client.send(:api_client)
|
|
25
|
+
config = api_client.config
|
|
26
|
+
|
|
27
|
+
url = "#{config.scheme}://#{config.host}#{path}"
|
|
28
|
+
token = client.access_token
|
|
29
|
+
|
|
30
|
+
conn = Faraday.new do |f|
|
|
31
|
+
f.request :json
|
|
32
|
+
f.response :json, parser_options: { symbolize_names: true }
|
|
33
|
+
f.adapter Faraday.default_adapter
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
response = conn.post(url) do |req|
|
|
37
|
+
req.headers["Authorization"] = "Bearer #{token}"
|
|
38
|
+
req.headers["Content-Type"] = "application/json"
|
|
39
|
+
req.body = params.to_json
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
unless response.success?
|
|
43
|
+
raise DoorFlow::ApiError.new(
|
|
44
|
+
code: response.status,
|
|
45
|
+
response_body: response.body
|
|
46
|
+
), "Element session creation failed (#{response.status})"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
response.body
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Proxy for creating admin widget sessions
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# session = client.admin_sessions.create(
|
|
57
|
+
# member: {
|
|
58
|
+
# external_id: "usr_123",
|
|
59
|
+
# name: "Alice Anderson",
|
|
60
|
+
# email: "alice@example.com"
|
|
61
|
+
# },
|
|
62
|
+
# credentials: ["mobile_access", "pin"]
|
|
63
|
+
# )
|
|
64
|
+
# session.token #=> "eyJhbGciOiJIUzI1NiJ9..."
|
|
65
|
+
# session.expires_in #=> 900
|
|
66
|
+
class AdminSessionsProxy < ElementSessionsProxy
|
|
67
|
+
def create(member:, credentials: nil)
|
|
68
|
+
create_session(widget_type: "admin", member: member, credentials: credentials)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Proxy for creating member widget sessions
|
|
73
|
+
#
|
|
74
|
+
# @example
|
|
75
|
+
# session = client.member_sessions.create(
|
|
76
|
+
# member: {
|
|
77
|
+
# external_id: "usr_123",
|
|
78
|
+
# name: "Alice Anderson",
|
|
79
|
+
# email: "alice@example.com"
|
|
80
|
+
# },
|
|
81
|
+
# credentials: ["mobile_access", "pin"]
|
|
82
|
+
# )
|
|
83
|
+
# session.token #=> "eyJhbGciOiJIUzI1NiJ9..."
|
|
84
|
+
# session.expires_in #=> 900
|
|
85
|
+
class MemberSessionsProxy < ElementSessionsProxy
|
|
86
|
+
def create(member:, credentials: nil)
|
|
87
|
+
create_session(widget_type: "member", member: member, credentials: credentials)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/doorflow/client.rb
CHANGED
|
@@ -129,6 +129,18 @@ module DoorFlow
|
|
|
129
129
|
@account ||= AccountProxy.new(self)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
# Access admin widget sessions
|
|
133
|
+
# @return [DoorFlow::Client::AdminSessionsProxy] admin sessions resource proxy
|
|
134
|
+
def admin_sessions
|
|
135
|
+
@admin_sessions ||= AdminSessionsProxy.new(self)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Access member widget sessions
|
|
139
|
+
# @return [DoorFlow::Client::MemberSessionsProxy] member sessions resource proxy
|
|
140
|
+
def member_sessions
|
|
141
|
+
@member_sessions ||= MemberSessionsProxy.new(self)
|
|
142
|
+
end
|
|
143
|
+
|
|
132
144
|
# ============================================
|
|
133
145
|
# Low-level API accessors (for advanced use)
|
|
134
146
|
# ============================================
|
|
@@ -261,3 +273,4 @@ require_relative 'client/reservations_proxy'
|
|
|
261
273
|
require_relative 'client/group_reservations_proxy'
|
|
262
274
|
require_relative 'client/notification_rules_proxy'
|
|
263
275
|
require_relative 'client/account_proxy'
|
|
276
|
+
require_relative 'client/element_sessions_proxy'
|
|
@@ -27,10 +27,11 @@ module DoorFlow
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Retrieve a credential by ID
|
|
30
|
+
# @param person_id [Integer] person ID (required by the API)
|
|
30
31
|
# @param id [String] credential ID
|
|
31
32
|
# @return [DoorFlow::Credential] the credential
|
|
32
|
-
def self.retrieve(id)
|
|
33
|
-
response = client.credentials_api.get_credential(id)
|
|
33
|
+
def self.retrieve(person_id, id)
|
|
34
|
+
response = client.credentials_api.get_credential(person_id, id)
|
|
34
35
|
from_api_model(response)
|
|
35
36
|
end
|
|
36
37
|
|
|
@@ -63,7 +64,7 @@ module DoorFlow
|
|
|
63
64
|
# Delete this credential
|
|
64
65
|
# @return [Boolean] true if successful
|
|
65
66
|
def delete
|
|
66
|
-
_client.credentials_api.delete_credential(@id)
|
|
67
|
+
_client.credentials_api.delete_credential(@person_id, @id)
|
|
67
68
|
true
|
|
68
69
|
rescue
|
|
69
70
|
false
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module DoorFlow
|
|
2
|
+
module Resources
|
|
3
|
+
# Represents a DoorFlow Elements session token
|
|
4
|
+
#
|
|
5
|
+
# Element sessions provide short-lived JWT tokens for embedding
|
|
6
|
+
# DoorFlow widgets (admin or member) in your application via iframes.
|
|
7
|
+
#
|
|
8
|
+
# @example Create an admin session
|
|
9
|
+
# client = DoorFlow::Client.new(access_token: 'your_token')
|
|
10
|
+
# session = client.element_sessions.create(
|
|
11
|
+
# person_id: 123,
|
|
12
|
+
# widget_type: 'admin'
|
|
13
|
+
# )
|
|
14
|
+
# session.token #=> "eyJhbGciOiJIUzI1NiJ9..."
|
|
15
|
+
# session.expires_in #=> 900
|
|
16
|
+
class ElementSession < Resource
|
|
17
|
+
# @return [String] the JWT session token
|
|
18
|
+
def token
|
|
19
|
+
@attributes[:token]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Integer] seconds until the token expires
|
|
23
|
+
def expires_in
|
|
24
|
+
@attributes[:expires_in]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -27,8 +27,8 @@ module DoorFlow
|
|
|
27
27
|
# # In your controller:
|
|
28
28
|
# handler.handle(
|
|
29
29
|
# payload: request.raw_post,
|
|
30
|
-
# signature: request.headers['
|
|
31
|
-
# timestamp: request.headers['
|
|
30
|
+
# signature: request.headers['Signature'],
|
|
31
|
+
# timestamp: request.headers['Timestamp']
|
|
32
32
|
# )
|
|
33
33
|
#
|
|
34
34
|
# @example With auto-fetch
|
|
@@ -82,8 +82,8 @@ module DoorFlow
|
|
|
82
82
|
# Process an incoming webhook request
|
|
83
83
|
#
|
|
84
84
|
# @param payload [String] Raw request body
|
|
85
|
-
# @param signature [String]
|
|
86
|
-
# @param timestamp [String]
|
|
85
|
+
# @param signature [String] Signature header
|
|
86
|
+
# @param timestamp [String] Timestamp header
|
|
87
87
|
# @return [Array<Event>] Processed events
|
|
88
88
|
# @raise [SignatureError] If signature verification fails
|
|
89
89
|
def handle(payload:, signature:, timestamp:)
|
|
@@ -108,8 +108,8 @@ module DoorFlow
|
|
|
108
108
|
def handle_request(request)
|
|
109
109
|
handle(
|
|
110
110
|
payload: extract_body(request),
|
|
111
|
-
signature: extract_header(request, '
|
|
112
|
-
timestamp: extract_header(request, '
|
|
111
|
+
signature: extract_header(request, 'Signature'),
|
|
112
|
+
timestamp: extract_header(request, 'Timestamp')
|
|
113
113
|
)
|
|
114
114
|
end
|
|
115
115
|
|
|
@@ -20,8 +20,8 @@ module DoorFlow
|
|
|
20
20
|
# @example Verifying a webhook
|
|
21
21
|
# events = DoorFlow::Webhooks::SignatureVerifier.verify(
|
|
22
22
|
# payload: request.raw_post,
|
|
23
|
-
# signature: request.headers['
|
|
24
|
-
# timestamp: request.headers['
|
|
23
|
+
# signature: request.headers['Signature'],
|
|
24
|
+
# timestamp: request.headers['Timestamp'],
|
|
25
25
|
# secret: ENV['DOORFLOW_WEBHOOK_SECRET']
|
|
26
26
|
# )
|
|
27
27
|
#
|
|
@@ -36,8 +36,8 @@ module DoorFlow
|
|
|
36
36
|
# Verify webhook signature and parse events
|
|
37
37
|
#
|
|
38
38
|
# @param payload [String] Raw request body
|
|
39
|
-
# @param signature [String]
|
|
40
|
-
# @param timestamp [String]
|
|
39
|
+
# @param signature [String] Signature header value
|
|
40
|
+
# @param timestamp [String] Timestamp header value
|
|
41
41
|
# @param secret [String] Webhook secret from DoorFlow dashboard
|
|
42
42
|
# @param tolerance [Integer] Maximum age in seconds (0 to disable)
|
|
43
43
|
# @return [Array<Event>] Parsed webhook events
|
|
@@ -109,7 +109,10 @@ module DoorFlow
|
|
|
109
109
|
|
|
110
110
|
def parse_events(payload)
|
|
111
111
|
data = JSON.parse(payload, symbolize_names: true)
|
|
112
|
-
events = data
|
|
112
|
+
events = case data
|
|
113
|
+
when Array then data
|
|
114
|
+
else data[:events] || [data]
|
|
115
|
+
end
|
|
113
116
|
events.map { |e| Event.new(e) }
|
|
114
117
|
rescue JSON::ParserError => e
|
|
115
118
|
raise SignatureError.new("Invalid JSON payload: #{e.message}")
|
data/lib/doorflow-api/version.rb
CHANGED
data/lib/doorflow.rb
CHANGED
|
@@ -28,6 +28,7 @@ require 'doorflow/resources/person'
|
|
|
28
28
|
require 'doorflow/resources/reservation'
|
|
29
29
|
require 'doorflow/resources/role'
|
|
30
30
|
require 'doorflow/resources/site'
|
|
31
|
+
require 'doorflow/resources/element_session'
|
|
31
32
|
|
|
32
33
|
# DoorFlow Ruby SDK for access control and door management
|
|
33
34
|
#
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe DoorFlow::Webhooks::Event do
|
|
6
|
+
let(:event_data) do
|
|
7
|
+
{
|
|
8
|
+
action: 'CREATE',
|
|
9
|
+
resource: 'https://api.doorflow.com/api/v1/accounts/1/events/42',
|
|
10
|
+
resource_type: 'Event',
|
|
11
|
+
resource_id: 42,
|
|
12
|
+
account_id: 1,
|
|
13
|
+
ack_token: 'abc123'
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
subject(:event) { described_class.new(event_data) }
|
|
18
|
+
|
|
19
|
+
describe '#initialize' do
|
|
20
|
+
it 'sets all attributes from the data hash' do
|
|
21
|
+
expect(event.action).to eq('CREATE')
|
|
22
|
+
expect(event.resource_url).to eq('https://api.doorflow.com/api/v1/accounts/1/events/42')
|
|
23
|
+
expect(event.resource_type).to eq('Event')
|
|
24
|
+
expect(event.resource_id).to eq('42')
|
|
25
|
+
expect(event.account_id).to eq(1)
|
|
26
|
+
expect(event.ack_token).to eq('abc123')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'converts resource_id to string' do
|
|
30
|
+
expect(event.resource_id).to be_a(String)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'handles nil resource_id' do
|
|
34
|
+
data = event_data.merge(resource_id: nil)
|
|
35
|
+
event = described_class.new(data)
|
|
36
|
+
expect(event.resource_id).to be_nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#pattern' do
|
|
41
|
+
it 'returns ResourceType.ACTION format' do
|
|
42
|
+
expect(event.pattern).to eq('Event.CREATE')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'handles different resource types' do
|
|
46
|
+
data = event_data.merge(resource_type: 'PersonCredential', action: 'UPDATE')
|
|
47
|
+
event = described_class.new(data)
|
|
48
|
+
expect(event.pattern).to eq('PersonCredential.UPDATE')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#create?' do
|
|
53
|
+
it 'returns true for CREATE action' do
|
|
54
|
+
expect(event.create?).to be true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns false for other actions' do
|
|
58
|
+
data = event_data.merge(action: 'DELETE')
|
|
59
|
+
expect(described_class.new(data).create?).to be false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#update?' do
|
|
64
|
+
it 'returns true for UPDATE action' do
|
|
65
|
+
data = event_data.merge(action: 'UPDATE')
|
|
66
|
+
expect(described_class.new(data).update?).to be true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns false for other actions' do
|
|
70
|
+
expect(event.update?).to be false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#delete?' do
|
|
75
|
+
it 'returns true for DELETE action' do
|
|
76
|
+
data = event_data.merge(action: 'DELETE')
|
|
77
|
+
expect(described_class.new(data).delete?).to be true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'returns false for other actions' do
|
|
81
|
+
expect(event.delete?).to be false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe '#to_h' do
|
|
86
|
+
it 'returns a hash with all attributes' do
|
|
87
|
+
expected = {
|
|
88
|
+
action: 'CREATE',
|
|
89
|
+
resource: 'https://api.doorflow.com/api/v1/accounts/1/events/42',
|
|
90
|
+
resource_type: 'Event',
|
|
91
|
+
resource_id: '42',
|
|
92
|
+
account_id: 1,
|
|
93
|
+
ack_token: 'abc123'
|
|
94
|
+
}
|
|
95
|
+
expect(event.to_h).to eq(expected)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'round-trips through Event.new' do
|
|
99
|
+
reconstructed = described_class.new(event.to_h)
|
|
100
|
+
expect(reconstructed.to_h).to eq(event.to_h)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe '#to_s' do
|
|
105
|
+
it 'includes pattern and resource_id' do
|
|
106
|
+
expect(event.to_s).to eq('Event.CREATE (42)')
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe '#inspect' do
|
|
111
|
+
it 'includes class name, pattern, and resource_id' do
|
|
112
|
+
expect(event.inspect).to include('DoorFlow::Webhooks::Event')
|
|
113
|
+
expect(event.inspect).to include('Event.CREATE')
|
|
114
|
+
expect(event.inspect).to include('42')
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative 'support'
|
|
5
|
+
|
|
6
|
+
RSpec.describe DoorFlow::Webhooks::Handler do
|
|
7
|
+
include WebhookSpecSupport
|
|
8
|
+
|
|
9
|
+
let(:secret) { WebhookSpecSupport::WEBHOOK_SECRET }
|
|
10
|
+
let(:timestamp) { Time.now.to_i.to_s }
|
|
11
|
+
let(:event_hash) { WebhookSpecSupport::SAMPLE_EVENT_HASH }
|
|
12
|
+
let(:payload) { JSON.generate([event_hash]) }
|
|
13
|
+
|
|
14
|
+
let(:signature) { compute_signature(timestamp, payload, secret) }
|
|
15
|
+
|
|
16
|
+
subject(:handler) { described_class.new(secret: secret) }
|
|
17
|
+
|
|
18
|
+
describe '#on' do
|
|
19
|
+
it 'returns self for method chaining' do
|
|
20
|
+
result = handler.on('Event.CREATE') { |e| }
|
|
21
|
+
expect(result).to be(handler)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'supports chained registrations' do
|
|
25
|
+
result = handler
|
|
26
|
+
.on('Event.CREATE') { |e| }
|
|
27
|
+
.on('Person.UPDATE') { |e| }
|
|
28
|
+
.on('*') { |e| }
|
|
29
|
+
|
|
30
|
+
expect(result).to be(handler)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'raises ArgumentError without a block' do
|
|
34
|
+
expect { handler.on('Event.CREATE') }.to raise_error(ArgumentError, /Block required/)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#handle' do
|
|
39
|
+
it 'dispatches events to matching handlers' do
|
|
40
|
+
received = []
|
|
41
|
+
handler.on('Event.CREATE') { |event| received << event }
|
|
42
|
+
|
|
43
|
+
handler.handle(payload: payload, signature: signature, timestamp: timestamp)
|
|
44
|
+
|
|
45
|
+
expect(received.length).to eq(1)
|
|
46
|
+
expect(received.first.pattern).to eq('Event.CREATE')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'does not dispatch to non-matching handlers' do
|
|
50
|
+
received = []
|
|
51
|
+
handler.on('Person.UPDATE') { |event| received << event }
|
|
52
|
+
|
|
53
|
+
handler.handle(payload: payload, signature: signature, timestamp: timestamp)
|
|
54
|
+
|
|
55
|
+
expect(received).to be_empty
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'dispatches to wildcard handler' do
|
|
59
|
+
received = []
|
|
60
|
+
handler.on('*') { |event| received << event }
|
|
61
|
+
|
|
62
|
+
handler.handle(payload: payload, signature: signature, timestamp: timestamp)
|
|
63
|
+
|
|
64
|
+
expect(received.length).to eq(1)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'dispatches to resource wildcard handler' do
|
|
68
|
+
received = []
|
|
69
|
+
handler.on('Event.*') { |event| received << event }
|
|
70
|
+
|
|
71
|
+
handler.handle(payload: payload, signature: signature, timestamp: timestamp)
|
|
72
|
+
|
|
73
|
+
expect(received.length).to eq(1)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'does not match wrong resource wildcard' do
|
|
77
|
+
received = []
|
|
78
|
+
handler.on('Person.*') { |event| received << event }
|
|
79
|
+
|
|
80
|
+
handler.handle(payload: payload, signature: signature, timestamp: timestamp)
|
|
81
|
+
|
|
82
|
+
expect(received).to be_empty
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'dispatches to multiple matching handlers' do
|
|
86
|
+
exact = []
|
|
87
|
+
wildcard = []
|
|
88
|
+
handler.on('Event.CREATE') { |event| exact << event }
|
|
89
|
+
handler.on('*') { |event| wildcard << event }
|
|
90
|
+
|
|
91
|
+
handler.handle(payload: payload, signature: signature, timestamp: timestamp)
|
|
92
|
+
|
|
93
|
+
expect(exact.length).to eq(1)
|
|
94
|
+
expect(wildcard.length).to eq(1)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns the parsed events' do
|
|
98
|
+
events = handler.handle(payload: payload, signature: signature, timestamp: timestamp)
|
|
99
|
+
|
|
100
|
+
expect(events).to be_an(Array)
|
|
101
|
+
expect(events.length).to eq(1)
|
|
102
|
+
expect(events.first).to be_a(DoorFlow::Webhooks::Event)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '#handle_request' do
|
|
107
|
+
context 'with a Rails-style request' do
|
|
108
|
+
it 'extracts Signature and Timestamp headers' do
|
|
109
|
+
headers = {
|
|
110
|
+
'Signature' => signature,
|
|
111
|
+
'Timestamp' => timestamp
|
|
112
|
+
}
|
|
113
|
+
request = double('rails_request',
|
|
114
|
+
raw_post: payload,
|
|
115
|
+
headers: headers
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
received = []
|
|
119
|
+
handler.on('Event.CREATE') { |event| received << event }
|
|
120
|
+
handler.handle_request(request)
|
|
121
|
+
|
|
122
|
+
expect(received.length).to eq(1)
|
|
123
|
+
expect(received.first.pattern).to eq('Event.CREATE')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
context 'with a Rack-style request' do
|
|
128
|
+
it 'extracts headers from the env hash' do
|
|
129
|
+
body = double('body')
|
|
130
|
+
allow(body).to receive(:read).and_return(payload)
|
|
131
|
+
|
|
132
|
+
request = double('rack_request',
|
|
133
|
+
body: body,
|
|
134
|
+
env: {
|
|
135
|
+
'HTTP_SIGNATURE' => signature,
|
|
136
|
+
'HTTP_TIMESTAMP' => timestamp
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
# Rack request doesn't respond to raw_post or headers
|
|
140
|
+
allow(request).to receive(:respond_to?).with(:raw_post).and_return(false)
|
|
141
|
+
allow(request).to receive(:respond_to?).with(:body).and_return(true)
|
|
142
|
+
allow(request).to receive(:respond_to?).with(:headers).and_return(false)
|
|
143
|
+
allow(request).to receive(:respond_to?).with(:env).and_return(true)
|
|
144
|
+
|
|
145
|
+
received = []
|
|
146
|
+
handler.on('Event.CREATE') { |event| received << event }
|
|
147
|
+
handler.handle_request(request)
|
|
148
|
+
|
|
149
|
+
expect(received.length).to eq(1)
|
|
150
|
+
expect(received.first.pattern).to eq('Event.CREATE')
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative 'support'
|
|
5
|
+
|
|
6
|
+
RSpec.describe DoorFlow::Webhooks::SignatureVerifier do
|
|
7
|
+
include WebhookSpecSupport
|
|
8
|
+
|
|
9
|
+
let(:secret) { WebhookSpecSupport::WEBHOOK_SECRET }
|
|
10
|
+
let(:timestamp) { Time.now.to_i.to_s }
|
|
11
|
+
let(:event_hash) { WebhookSpecSupport::SAMPLE_EVENT_HASH }
|
|
12
|
+
|
|
13
|
+
describe '.verify' do
|
|
14
|
+
context 'with a root-level array payload (documented format)' do
|
|
15
|
+
let(:payload) { JSON.generate([event_hash]) }
|
|
16
|
+
let(:signature) { compute_signature(timestamp, payload, secret) }
|
|
17
|
+
|
|
18
|
+
it 'returns an array of Event objects' do
|
|
19
|
+
events = described_class.verify(
|
|
20
|
+
payload: payload,
|
|
21
|
+
signature: signature,
|
|
22
|
+
timestamp: timestamp,
|
|
23
|
+
secret: secret
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
expect(events).to be_an(Array)
|
|
27
|
+
expect(events.length).to eq(1)
|
|
28
|
+
expect(events.first).to be_a(DoorFlow::Webhooks::Event)
|
|
29
|
+
expect(events.first.resource_type).to eq('Event')
|
|
30
|
+
expect(events.first.action).to eq('CREATE')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context 'with multiple events in an array' do
|
|
35
|
+
let(:second_event) do
|
|
36
|
+
{
|
|
37
|
+
action: 'UPDATE',
|
|
38
|
+
resource: 'https://api.doorflow.com/api/v1/accounts/1/people/99',
|
|
39
|
+
resource_type: 'Person',
|
|
40
|
+
resource_id: 99,
|
|
41
|
+
account_id: 1,
|
|
42
|
+
ack_token: 'def456'
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
let(:payload) { JSON.generate([event_hash, second_event]) }
|
|
46
|
+
let(:signature) { compute_signature(timestamp, payload, secret) }
|
|
47
|
+
|
|
48
|
+
it 'returns all events' do
|
|
49
|
+
events = described_class.verify(
|
|
50
|
+
payload: payload,
|
|
51
|
+
signature: signature,
|
|
52
|
+
timestamp: timestamp,
|
|
53
|
+
secret: secret
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
expect(events.length).to eq(2)
|
|
57
|
+
expect(events[0].pattern).to eq('Event.CREATE')
|
|
58
|
+
expect(events[1].pattern).to eq('Person.UPDATE')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context 'with a single event object payload' do
|
|
63
|
+
let(:payload) { JSON.generate(event_hash) }
|
|
64
|
+
let(:signature) { compute_signature(timestamp, payload, secret) }
|
|
65
|
+
|
|
66
|
+
it 'wraps the single event in an array' do
|
|
67
|
+
events = described_class.verify(
|
|
68
|
+
payload: payload,
|
|
69
|
+
signature: signature,
|
|
70
|
+
timestamp: timestamp,
|
|
71
|
+
secret: secret
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expect(events.length).to eq(1)
|
|
75
|
+
expect(events.first.pattern).to eq('Event.CREATE')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context 'with an events-key payload (backwards compat)' do
|
|
80
|
+
let(:payload) { JSON.generate({ events: [event_hash] }) }
|
|
81
|
+
let(:signature) { compute_signature(timestamp, payload, secret) }
|
|
82
|
+
|
|
83
|
+
it 'extracts events from the events key' do
|
|
84
|
+
events = described_class.verify(
|
|
85
|
+
payload: payload,
|
|
86
|
+
signature: signature,
|
|
87
|
+
timestamp: timestamp,
|
|
88
|
+
secret: secret
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
expect(events.length).to eq(1)
|
|
92
|
+
expect(events.first.pattern).to eq('Event.CREATE')
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
context 'with an invalid signature' do
|
|
97
|
+
let(:payload) { JSON.generate([event_hash]) }
|
|
98
|
+
|
|
99
|
+
it 'raises SignatureError' do
|
|
100
|
+
expect {
|
|
101
|
+
described_class.verify(
|
|
102
|
+
payload: payload,
|
|
103
|
+
signature: 'invalid_signature_value',
|
|
104
|
+
timestamp: timestamp,
|
|
105
|
+
secret: secret
|
|
106
|
+
)
|
|
107
|
+
}.to raise_error(DoorFlow::Webhooks::SignatureError, /Signature mismatch/)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
context 'with an expired timestamp' do
|
|
112
|
+
let(:old_timestamp) { (Time.now.to_i - 600).to_s }
|
|
113
|
+
let(:payload) { JSON.generate([event_hash]) }
|
|
114
|
+
let(:signature) { compute_signature(old_timestamp, payload, secret) }
|
|
115
|
+
|
|
116
|
+
it 'raises SignatureError' do
|
|
117
|
+
expect {
|
|
118
|
+
described_class.verify(
|
|
119
|
+
payload: payload,
|
|
120
|
+
signature: signature,
|
|
121
|
+
timestamp: old_timestamp,
|
|
122
|
+
secret: secret
|
|
123
|
+
)
|
|
124
|
+
}.to raise_error(DoorFlow::Webhooks::SignatureError, /too old/)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
context 'with tolerance disabled' do
|
|
129
|
+
let(:old_timestamp) { (Time.now.to_i - 600).to_s }
|
|
130
|
+
let(:payload) { JSON.generate([event_hash]) }
|
|
131
|
+
let(:signature) { compute_signature(old_timestamp, payload, secret) }
|
|
132
|
+
|
|
133
|
+
it 'does not check timestamp' do
|
|
134
|
+
events = described_class.verify(
|
|
135
|
+
payload: payload,
|
|
136
|
+
signature: signature,
|
|
137
|
+
timestamp: old_timestamp,
|
|
138
|
+
secret: secret,
|
|
139
|
+
tolerance: 0
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
expect(events.length).to eq(1)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
context 'with missing inputs' do
|
|
147
|
+
let(:payload) { JSON.generate([event_hash]) }
|
|
148
|
+
let(:signature) { compute_signature(timestamp, payload, secret) }
|
|
149
|
+
|
|
150
|
+
it 'raises SignatureError for missing payload' do
|
|
151
|
+
expect {
|
|
152
|
+
described_class.verify(payload: nil, signature: signature, timestamp: timestamp, secret: secret)
|
|
153
|
+
}.to raise_error(DoorFlow::Webhooks::SignatureError, /Missing payload/)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'raises SignatureError for empty payload' do
|
|
157
|
+
expect {
|
|
158
|
+
described_class.verify(payload: '', signature: signature, timestamp: timestamp, secret: secret)
|
|
159
|
+
}.to raise_error(DoorFlow::Webhooks::SignatureError, /Missing payload/)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'raises SignatureError for missing signature' do
|
|
163
|
+
expect {
|
|
164
|
+
described_class.verify(payload: payload, signature: nil, timestamp: timestamp, secret: secret)
|
|
165
|
+
}.to raise_error(DoorFlow::Webhooks::SignatureError, /Missing signature/)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'raises SignatureError for missing timestamp' do
|
|
169
|
+
expect {
|
|
170
|
+
described_class.verify(payload: payload, signature: signature, timestamp: nil, secret: secret)
|
|
171
|
+
}.to raise_error(DoorFlow::Webhooks::SignatureError, /Missing timestamp/)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'raises SignatureError for missing secret' do
|
|
175
|
+
expect {
|
|
176
|
+
described_class.verify(payload: payload, signature: signature, timestamp: timestamp, secret: nil)
|
|
177
|
+
}.to raise_error(DoorFlow::Webhooks::SignatureError, /Missing secret/)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
context 'with malformed JSON' do
|
|
182
|
+
let(:payload) { 'not valid json{{{' }
|
|
183
|
+
let(:signature) { compute_signature(timestamp, payload, secret) }
|
|
184
|
+
|
|
185
|
+
it 'raises SignatureError' do
|
|
186
|
+
expect {
|
|
187
|
+
described_class.verify(
|
|
188
|
+
payload: payload,
|
|
189
|
+
signature: signature,
|
|
190
|
+
timestamp: timestamp,
|
|
191
|
+
secret: secret
|
|
192
|
+
)
|
|
193
|
+
}.to raise_error(DoorFlow::Webhooks::SignatureError, /Invalid JSON/)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
describe '.compute_signature' do
|
|
199
|
+
it 'returns a hex-encoded HMAC-SHA256 digest' do
|
|
200
|
+
sig = described_class.compute_signature('12345', '{"test":true}', 'secret')
|
|
201
|
+
expect(sig).to match(/\A[a-f0-9]{64}\z/)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'uses timestamp.payload format' do
|
|
205
|
+
expected = OpenSSL::HMAC.hexdigest('SHA256', 'secret', '12345.{"test":true}')
|
|
206
|
+
sig = described_class.compute_signature('12345', '{"test":true}', 'secret')
|
|
207
|
+
expect(sig).to eq(expected)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebhookSpecSupport
|
|
4
|
+
WEBHOOK_SECRET = 'whsec_test_secret_key'
|
|
5
|
+
|
|
6
|
+
SAMPLE_EVENT_HASH = {
|
|
7
|
+
action: 'CREATE',
|
|
8
|
+
resource: 'https://api.doorflow.com/api/v1/accounts/1/events/42',
|
|
9
|
+
resource_type: 'Event',
|
|
10
|
+
resource_id: 42,
|
|
11
|
+
account_id: 1,
|
|
12
|
+
ack_token: 'abc123'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def compute_signature(timestamp, payload, secret)
|
|
16
|
+
OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{payload}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Regression specs for bugs reported by William:
|
|
4
|
+
# - retrieve and delete were calling get_credential / delete_credential
|
|
5
|
+
# with only (id), omitting the required person_id argument.
|
|
6
|
+
# - CredentialsProxy#delete had a dead else-branch that always failed silently.
|
|
7
|
+
RSpec.describe 'Credential person_id argument fixes' do
|
|
8
|
+
let(:person_id) { 456 }
|
|
9
|
+
let(:credential_id) { 'abc123' }
|
|
10
|
+
|
|
11
|
+
let(:credential_response) do
|
|
12
|
+
double('CredentialResponse',
|
|
13
|
+
to_hash: { 'id' => credential_id, 'person_id' => person_id,
|
|
14
|
+
'credential_type_id' => 1, 'label' => 'Card' }
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:mock_credentials_api) { double('CredentialsApi') }
|
|
19
|
+
let(:mock_client) { double('Client', credentials_api: mock_credentials_api) }
|
|
20
|
+
|
|
21
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
# DoorFlow::Resources::Credential.retrieve
|
|
23
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
describe 'DoorFlow::Resources::Credential.retrieve' do
|
|
25
|
+
before { allow(DoorFlow::Resources::Credential).to receive(:client).and_return(mock_client) }
|
|
26
|
+
|
|
27
|
+
it 'passes person_id and id to get_credential' do
|
|
28
|
+
expect(mock_credentials_api).to receive(:get_credential)
|
|
29
|
+
.with(person_id, credential_id)
|
|
30
|
+
.and_return(credential_response)
|
|
31
|
+
|
|
32
|
+
DoorFlow::Resources::Credential.retrieve(person_id, credential_id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'returns a Credential resource' do
|
|
36
|
+
allow(mock_credentials_api).to receive(:get_credential)
|
|
37
|
+
.with(person_id, credential_id)
|
|
38
|
+
.and_return(credential_response)
|
|
39
|
+
|
|
40
|
+
result = DoorFlow::Resources::Credential.retrieve(person_id, credential_id)
|
|
41
|
+
expect(result).to be_a(DoorFlow::Resources::Credential)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
# DoorFlow::Resources::Credential#delete
|
|
47
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
describe 'DoorFlow::Resources::Credential#delete' do
|
|
49
|
+
let(:credential) do
|
|
50
|
+
DoorFlow::Resources::Credential.new(
|
|
51
|
+
{ 'id' => credential_id, 'person_id' => person_id },
|
|
52
|
+
client: mock_client
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'passes person_id and id to delete_credential' do
|
|
57
|
+
expect(mock_credentials_api).to receive(:delete_credential)
|
|
58
|
+
.with(person_id, credential_id)
|
|
59
|
+
.and_return(nil)
|
|
60
|
+
|
|
61
|
+
credential.delete
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'returns true on success' do
|
|
65
|
+
allow(mock_credentials_api).to receive(:delete_credential)
|
|
66
|
+
.with(person_id, credential_id)
|
|
67
|
+
.and_return(nil)
|
|
68
|
+
|
|
69
|
+
expect(credential.delete).to eq(true)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
# DoorFlow::Client::CredentialsProxy#retrieve
|
|
75
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
describe 'DoorFlow::Client::CredentialsProxy#retrieve' do
|
|
77
|
+
let(:proxy) { DoorFlow::Client::CredentialsProxy.new(mock_client) }
|
|
78
|
+
|
|
79
|
+
it 'passes person_id and id to get_credential' do
|
|
80
|
+
expect(mock_credentials_api).to receive(:get_credential)
|
|
81
|
+
.with(person_id, credential_id)
|
|
82
|
+
.and_return(credential_response)
|
|
83
|
+
|
|
84
|
+
proxy.retrieve(person_id, credential_id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'returns a Credential resource' do
|
|
88
|
+
allow(mock_credentials_api).to receive(:get_credential)
|
|
89
|
+
.with(person_id, credential_id)
|
|
90
|
+
.and_return(credential_response)
|
|
91
|
+
|
|
92
|
+
result = proxy.retrieve(person_id, credential_id)
|
|
93
|
+
expect(result).to be_a(DoorFlow::Resources::Credential)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
# DoorFlow::Client::CredentialsProxy#delete
|
|
99
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
describe 'DoorFlow::Client::CredentialsProxy#delete' do
|
|
101
|
+
let(:proxy) { DoorFlow::Client::CredentialsProxy.new(mock_client) }
|
|
102
|
+
|
|
103
|
+
it 'passes person_id and id to delete_credential and returns true' do
|
|
104
|
+
expect(mock_credentials_api).to receive(:delete_credential)
|
|
105
|
+
.with(person_id, credential_id)
|
|
106
|
+
.and_return(nil)
|
|
107
|
+
|
|
108
|
+
expect(proxy.delete(credential_id, person_id: person_id)).to eq(true)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'raises ArgumentError when person_id is not supplied' do
|
|
112
|
+
expect { proxy.delete(credential_id) }.to raise_error(ArgumentError)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: doorflow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- NetNodes Ltd
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-02-
|
|
10
|
+
date: 2026-02-24 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: faraday
|
|
@@ -282,6 +282,7 @@ files:
|
|
|
282
282
|
- lib/doorflow/client/channels_proxy.rb
|
|
283
283
|
- lib/doorflow/client/credential_types_proxy.rb
|
|
284
284
|
- lib/doorflow/client/credentials_proxy.rb
|
|
285
|
+
- lib/doorflow/client/element_sessions_proxy.rb
|
|
285
286
|
- lib/doorflow/client/events_proxy.rb
|
|
286
287
|
- lib/doorflow/client/group_reservations_proxy.rb
|
|
287
288
|
- lib/doorflow/client/groups_proxy.rb
|
|
@@ -305,6 +306,7 @@ files:
|
|
|
305
306
|
- lib/doorflow/resources/channel.rb
|
|
306
307
|
- lib/doorflow/resources/credential.rb
|
|
307
308
|
- lib/doorflow/resources/credential_type.rb
|
|
309
|
+
- lib/doorflow/resources/element_session.rb
|
|
308
310
|
- lib/doorflow/resources/event.rb
|
|
309
311
|
- lib/doorflow/resources/group.rb
|
|
310
312
|
- lib/doorflow/resources/group_reservation.rb
|
|
@@ -323,6 +325,10 @@ files:
|
|
|
323
325
|
- spec/api/group_reservations_api_spec.rb
|
|
324
326
|
- spec/api/oauth_api_spec.rb
|
|
325
327
|
- spec/doorflow/client_spec.rb
|
|
328
|
+
- spec/doorflow/webhooks/event_spec.rb
|
|
329
|
+
- spec/doorflow/webhooks/handler_spec.rb
|
|
330
|
+
- spec/doorflow/webhooks/signature_verifier_spec.rb
|
|
331
|
+
- spec/doorflow/webhooks/support.rb
|
|
326
332
|
- spec/doorflow_spec.rb
|
|
327
333
|
- spec/fixtures/vcr_cassettes/channel/list.yml
|
|
328
334
|
- spec/fixtures/vcr_cassettes/channel/retrieve.yml
|
|
@@ -390,6 +396,7 @@ files:
|
|
|
390
396
|
- spec/models/revoke_token403_response_spec.rb
|
|
391
397
|
- spec/models/site_site_ips_inner_spec.rb
|
|
392
398
|
- spec/models/unlock_channel_request_spec.rb
|
|
399
|
+
- spec/resources/credential_bugs_spec.rb
|
|
393
400
|
- spec/spec_helper.rb
|
|
394
401
|
homepage: https://www.doorflow.com
|
|
395
402
|
licenses:
|
|
@@ -425,6 +432,10 @@ test_files:
|
|
|
425
432
|
- spec/api/group_reservations_api_spec.rb
|
|
426
433
|
- spec/api/oauth_api_spec.rb
|
|
427
434
|
- spec/doorflow/client_spec.rb
|
|
435
|
+
- spec/doorflow/webhooks/event_spec.rb
|
|
436
|
+
- spec/doorflow/webhooks/handler_spec.rb
|
|
437
|
+
- spec/doorflow/webhooks/signature_verifier_spec.rb
|
|
438
|
+
- spec/doorflow/webhooks/support.rb
|
|
428
439
|
- spec/doorflow_spec.rb
|
|
429
440
|
- spec/fixtures/vcr_cassettes/channel/list.yml
|
|
430
441
|
- spec/fixtures/vcr_cassettes/channel/retrieve.yml
|
|
@@ -492,4 +503,5 @@ test_files:
|
|
|
492
503
|
- spec/models/revoke_token403_response_spec.rb
|
|
493
504
|
- spec/models/site_site_ips_inner_spec.rb
|
|
494
505
|
- spec/models/unlock_channel_request_spec.rb
|
|
506
|
+
- spec/resources/credential_bugs_spec.rb
|
|
495
507
|
- spec/spec_helper.rb
|