hivehook 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/LICENSE +21 -0
- data/README.md +53 -0
- data/lib/hivehook/errors.rb +36 -0
- data/lib/hivehook/resources/alert_rule_service.rb +44 -0
- data/lib/hivehook/resources/api_key_service.rb +34 -0
- data/lib/hivehook/resources/application_service.rb +39 -0
- data/lib/hivehook/resources/audit_log_service.rb +24 -0
- data/lib/hivehook/resources/base_service.rb +22 -0
- data/lib/hivehook/resources/bookmark_service.rb +29 -0
- data/lib/hivehook/resources/delivery_service.rb +24 -0
- data/lib/hivehook/resources/destination_service.rb +44 -0
- data/lib/hivehook/resources/dlq_service.rb +39 -0
- data/lib/hivehook/resources/endpoint_service.rb +44 -0
- data/lib/hivehook/resources/event_service.rb +24 -0
- data/lib/hivehook/resources/event_type_schema_service.rb +39 -0
- data/lib/hivehook/resources/message_service.rb +39 -0
- data/lib/hivehook/resources/organization_service.rb +74 -0
- data/lib/hivehook/resources/outbound_delivery_service.rb +24 -0
- data/lib/hivehook/resources/outbound_dlq_service.rb +39 -0
- data/lib/hivehook/resources/portal_service.rb +12 -0
- data/lib/hivehook/resources/source_service.rb +49 -0
- data/lib/hivehook/resources/status_service.rb +12 -0
- data/lib/hivehook/resources/stream_consumer_service.rb +40 -0
- data/lib/hivehook/resources/stream_service.rb +39 -0
- data/lib/hivehook/resources/stream_sink_service.rb +40 -0
- data/lib/hivehook/resources/subscription_service.rb +39 -0
- data/lib/hivehook/resources/transformation_service.rb +44 -0
- data/lib/hivehook/resources/user_service.rb +39 -0
- data/lib/hivehook/transport.rb +151 -0
- data/lib/hivehook/version.rb +5 -0
- data/lib/hivehook/webhook.rb +63 -0
- data/lib/hivehook.rb +76 -0
- metadata +110 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class OutboundDeliveryService < BaseService
|
|
6
|
+
FRAGMENT = "id messageId endpointId status attempts maxAttempts nextAttemptAt createdAt"
|
|
7
|
+
|
|
8
|
+
def list(options = {})
|
|
9
|
+
query = "query($messageId: UUID, $endpointId: UUID, $status: DeliveryStatus, $search: String, $limit: Int, $offset: Int, $after: String, $first: Int) {
|
|
10
|
+
outboundDeliveries(messageId: $messageId, endpointId: $endpointId, status: $status, search: $search, limit: $limit, offset: $offset, after: $after, first: $first) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
@transport.execute(query, build_variables(options, %w[messageId endpointId status search limit offset after first]))["outboundDeliveries"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(id)
|
|
19
|
+
query = "query($id: UUID!) { outboundDelivery(id: $id) { #{FRAGMENT} } }"
|
|
20
|
+
@transport.execute(query, { "id" => id })["outboundDelivery"]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class OutboundDLQService < BaseService
|
|
6
|
+
FRAGMENT = "id deliveryId messageId lastError replayedAt createdAt"
|
|
7
|
+
|
|
8
|
+
def list(options = {})
|
|
9
|
+
query = "query($messageId: UUID, $replayed: Boolean, $search: String, $limit: Int, $offset: Int, $after: String, $first: Int) {
|
|
10
|
+
outboundDlqEntries(messageId: $messageId, replayed: $replayed, search: $search, limit: $limit, offset: $offset, after: $after, first: $first) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
@transport.execute(query, build_variables(options, %w[messageId replayed search limit offset after first]))["outboundDlqEntries"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(id)
|
|
19
|
+
query = "query($id: UUID!) { outboundDlqEntry(id: $id) { #{FRAGMENT} } }"
|
|
20
|
+
@transport.execute(query, { "id" => id })["outboundDlqEntry"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def replay(id)
|
|
24
|
+
query = "mutation($id: UUID!) { replayOutboundDlqEntry(id: $id) }"
|
|
25
|
+
@transport.execute(query, { "id" => id })["replayOutboundDlqEntry"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def replay_all
|
|
29
|
+
query = "mutation { replayAllOutboundDlq { deliveries } }"
|
|
30
|
+
@transport.execute(query)["replayAllOutboundDlq"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def purge(older_than)
|
|
34
|
+
query = "mutation($olderThan: String!) { purgeOutboundDlq(olderThan: $olderThan) { purged } }"
|
|
35
|
+
@transport.execute(query, { "olderThan" => older_than })["purgeOutboundDlq"]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class PortalService < BaseService
|
|
6
|
+
def generate_token(application_id)
|
|
7
|
+
query = "mutation($applicationId: UUID!) { generatePortalToken(applicationId: $applicationId) { token expiresAt } }"
|
|
8
|
+
@transport.execute(query, { "applicationId" => application_id })["generatePortalToken"]
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class SourceService < BaseService
|
|
6
|
+
FRAGMENT = "id name slug providerType verifyConfig status rateLimitRps spikeProtection maxIngestRps brokerConfig responseConfig { statusCode body contentType } dedupConfig { strategy fields window } createdAt"
|
|
7
|
+
|
|
8
|
+
def list(options = {})
|
|
9
|
+
query = "query($status: SourceStatus, $providerType: String, $search: String, $limit: Int, $offset: Int, $after: String, $first: Int) {
|
|
10
|
+
sources(status: $status, providerType: $providerType, search: $search, limit: $limit, offset: $offset, after: $after, first: $first) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
@transport.execute(query, build_variables(options, %w[status providerType search limit offset after first]))["sources"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(id)
|
|
19
|
+
query = "query($id: UUID!) { source(id: $id) { #{FRAGMENT} } }"
|
|
20
|
+
@transport.execute(query, { "id" => id })["source"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create(input)
|
|
24
|
+
query = "mutation($input: CreateSourceInput!) { createSource(input: $input) { #{FRAGMENT} } }"
|
|
25
|
+
@transport.execute(query, { "input" => input })["createSource"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update(id, input)
|
|
29
|
+
query = "mutation($id: UUID!, $input: UpdateSourceInput!) { updateSource(id: $id, input: $input) { #{FRAGMENT} } }"
|
|
30
|
+
@transport.execute(query, { "id" => id, "input" => input })["updateSource"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete(id)
|
|
34
|
+
query = "mutation($id: UUID!) { deleteSource(id: $id) }"
|
|
35
|
+
@transport.execute(query, { "id" => id })["deleteSource"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rotate_secret(id)
|
|
39
|
+
query = "mutation($id: UUID!) { rotateSourceSecret(id: $id) { #{FRAGMENT} } }"
|
|
40
|
+
@transport.execute(query, { "id" => id })["rotateSourceSecret"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def clear_secondary_secret(id)
|
|
44
|
+
query = "mutation($id: UUID!) { clearSourceSecondarySecret(id: $id) { #{FRAGMENT} } }"
|
|
45
|
+
@transport.execute(query, { "id" => id })["clearSourceSecondarySecret"]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class StatusService < BaseService
|
|
6
|
+
def get
|
|
7
|
+
query = "query { status { status dlqSize outboundDlqSize queueDepth activeWorkers totalWorkers uptime version sourcesTotal destinationsTotal subscriptionsTotal eventsTotal eventsFailed deliveriesTotal deliveriesPending deliveriesDelivered messagesTotal outboundDeliveriesTotal outboundDeliveriesPending outboundDeliveriesFailed } }"
|
|
8
|
+
@transport.execute(query)["status"]
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class StreamConsumerService < BaseService
|
|
6
|
+
FRAGMENT = "id streamId name cursorSequence createdAt updatedAt"
|
|
7
|
+
|
|
8
|
+
def list(stream_id, options = {})
|
|
9
|
+
query = "query($streamId: UUID!, $search: String, $limit: Int, $offset: Int, $after: String, $first: Int) {
|
|
10
|
+
streamConsumers(streamId: $streamId, search: $search, limit: $limit, offset: $offset, after: $after, first: $first) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
vars = { "streamId" => stream_id }.merge(build_variables(options, %w[search limit offset after first]))
|
|
16
|
+
@transport.execute(query, vars)["streamConsumers"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get(id)
|
|
20
|
+
query = "query($id: UUID!) { streamConsumer(id: $id) { #{FRAGMENT} } }"
|
|
21
|
+
@transport.execute(query, { "id" => id })["streamConsumer"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create(input)
|
|
25
|
+
query = "mutation($input: CreateStreamConsumerInput!) { createStreamConsumer(input: $input) { #{FRAGMENT} } }"
|
|
26
|
+
@transport.execute(query, { "input" => input })["createStreamConsumer"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def delete(id)
|
|
30
|
+
query = "mutation($id: UUID!) { deleteStreamConsumer(id: $id) }"
|
|
31
|
+
@transport.execute(query, { "id" => id })["deleteStreamConsumer"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def advance_cursor(id, sequence)
|
|
35
|
+
query = "mutation($id: UUID!, $sequence: Int!) { advanceConsumerCursor(id: $id, sequence: $sequence) { #{FRAGMENT} } }"
|
|
36
|
+
@transport.execute(query, { "id" => id, "sequence" => sequence })["advanceConsumerCursor"]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class StreamService < BaseService
|
|
6
|
+
FRAGMENT = "id applicationId name status retentionDays createdAt"
|
|
7
|
+
|
|
8
|
+
def list(options = {})
|
|
9
|
+
query = "query($applicationId: UUID, $status: StreamStatus, $search: String, $limit: Int, $offset: Int, $after: String, $first: Int) {
|
|
10
|
+
streams(applicationId: $applicationId, status: $status, search: $search, limit: $limit, offset: $offset, after: $after, first: $first) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
@transport.execute(query, build_variables(options, %w[applicationId status search limit offset after first]))["streams"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(id)
|
|
19
|
+
query = "query($id: UUID!) { stream(id: $id) { #{FRAGMENT} } }"
|
|
20
|
+
@transport.execute(query, { "id" => id })["stream"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create(input)
|
|
24
|
+
query = "mutation($input: CreateStreamInput!) { createStream(input: $input) { #{FRAGMENT} } }"
|
|
25
|
+
@transport.execute(query, { "input" => input })["createStream"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update(id, input)
|
|
29
|
+
query = "mutation($id: UUID!, $input: UpdateStreamInput!) { updateStream(id: $id, input: $input) { #{FRAGMENT} } }"
|
|
30
|
+
@transport.execute(query, { "id" => id, "input" => input })["updateStream"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete(id)
|
|
34
|
+
query = "mutation($id: UUID!) { deleteStream(id: $id) }"
|
|
35
|
+
@transport.execute(query, { "id" => id })["deleteStream"]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class StreamSinkService < BaseService
|
|
6
|
+
FRAGMENT = "id streamId name sinkType config batchSize flushInterval cursorSequence status lastFlushedAt createdAt"
|
|
7
|
+
|
|
8
|
+
def list(stream_id, options = {})
|
|
9
|
+
query = "query($streamId: UUID!, $status: SinkStatus, $search: String, $limit: Int, $offset: Int, $after: String, $first: Int) {
|
|
10
|
+
streamSinks(streamId: $streamId, status: $status, search: $search, limit: $limit, offset: $offset, after: $after, first: $first) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
vars = { "streamId" => stream_id }.merge(build_variables(options, %w[status search limit offset after first]))
|
|
16
|
+
@transport.execute(query, vars)["streamSinks"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get(id)
|
|
20
|
+
query = "query($id: UUID!) { streamSink(id: $id) { #{FRAGMENT} } }"
|
|
21
|
+
@transport.execute(query, { "id" => id })["streamSink"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create(input)
|
|
25
|
+
query = "mutation($input: CreateStreamSinkInput!) { createStreamSink(input: $input) { #{FRAGMENT} } }"
|
|
26
|
+
@transport.execute(query, { "input" => input })["createStreamSink"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def update(id, input)
|
|
30
|
+
query = "mutation($id: UUID!, $input: UpdateStreamSinkInput!) { updateStreamSink(id: $id, input: $input) { #{FRAGMENT} } }"
|
|
31
|
+
@transport.execute(query, { "id" => id, "input" => input })["updateStreamSink"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def delete(id)
|
|
35
|
+
query = "mutation($id: UUID!) { deleteStreamSink(id: $id) }"
|
|
36
|
+
@transport.execute(query, { "id" => id })["deleteStreamSink"]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class SubscriptionService < BaseService
|
|
6
|
+
FRAGMENT = "id name sourceId destinationId filterConfig enabled createdAt"
|
|
7
|
+
|
|
8
|
+
def list(options = {})
|
|
9
|
+
query = "query($sourceId: UUID, $destinationId: UUID, $enabled: Boolean, $search: String, $limit: Int, $offset: Int, $after: String, $first: Int) {
|
|
10
|
+
subscriptions(sourceId: $sourceId, destinationId: $destinationId, enabled: $enabled, search: $search, limit: $limit, offset: $offset, after: $after, first: $first) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
@transport.execute(query, build_variables(options, %w[sourceId destinationId enabled search limit offset after first]))["subscriptions"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(id)
|
|
19
|
+
query = "query($id: UUID!) { subscription(id: $id) { #{FRAGMENT} } }"
|
|
20
|
+
@transport.execute(query, { "id" => id })["subscription"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create(input)
|
|
24
|
+
query = "mutation($input: CreateSubscriptionInput!) { createSubscription(input: $input) { #{FRAGMENT} } }"
|
|
25
|
+
@transport.execute(query, { "input" => input })["createSubscription"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update(id, input)
|
|
29
|
+
query = "mutation($id: UUID!, $input: UpdateSubscriptionInput!) { updateSubscription(id: $id, input: $input) { #{FRAGMENT} } }"
|
|
30
|
+
@transport.execute(query, { "id" => id, "input" => input })["updateSubscription"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete(id)
|
|
34
|
+
query = "mutation($id: UUID!) { deleteSubscription(id: $id) }"
|
|
35
|
+
@transport.execute(query, { "id" => id })["deleteSubscription"]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class TransformationService < BaseService
|
|
6
|
+
FRAGMENT = "id name description code enabled failOpen timeoutMs createdAt updatedAt"
|
|
7
|
+
|
|
8
|
+
def list(options = {})
|
|
9
|
+
query = "query($enabled: Boolean, $search: String, $limit: Int, $offset: Int, $after: String, $first: Int) {
|
|
10
|
+
transformations(enabled: $enabled, search: $search, limit: $limit, offset: $offset, after: $after, first: $first) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
@transport.execute(query, build_variables(options, %w[enabled search limit offset after first]))["transformations"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(id)
|
|
19
|
+
query = "query($id: UUID!) { transformation(id: $id) { #{FRAGMENT} } }"
|
|
20
|
+
@transport.execute(query, { "id" => id })["transformation"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create(input)
|
|
24
|
+
query = "mutation($input: CreateTransformationInput!) { createTransformation(input: $input) { #{FRAGMENT} } }"
|
|
25
|
+
@transport.execute(query, { "input" => input })["createTransformation"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update(id, input)
|
|
29
|
+
query = "mutation($id: UUID!, $input: UpdateTransformationInput!) { updateTransformation(id: $id, input: $input) { #{FRAGMENT} } }"
|
|
30
|
+
@transport.execute(query, { "id" => id, "input" => input })["updateTransformation"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete(id)
|
|
34
|
+
query = "mutation($id: UUID!) { deleteTransformation(id: $id) }"
|
|
35
|
+
@transport.execute(query, { "id" => id })["deleteTransformation"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test(input)
|
|
39
|
+
query = "mutation($input: TestTransformationInput!) { testTransformation(input: $input) { success output error durationMs } }"
|
|
40
|
+
@transport.execute(query, { "input" => input })["testTransformation"]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hivehook
|
|
4
|
+
module Resources
|
|
5
|
+
class UserService < BaseService
|
|
6
|
+
FRAGMENT = "id organizationId email name role lastLoginAt createdAt updatedAt"
|
|
7
|
+
|
|
8
|
+
def list(options = {})
|
|
9
|
+
query = "query($organizationId: UUID, $search: String, $limit: Int, $offset: Int) {
|
|
10
|
+
users(organizationId: $organizationId, search: $search, limit: $limit, offset: $offset) {
|
|
11
|
+
nodes { #{FRAGMENT} }
|
|
12
|
+
pageInfo { total limit offset endCursor hasNextPage }
|
|
13
|
+
}
|
|
14
|
+
}"
|
|
15
|
+
@transport.execute(query, build_variables(options, %w[organizationId search limit offset]))["users"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def me
|
|
19
|
+
query = "query { me { #{FRAGMENT} } }"
|
|
20
|
+
@transport.execute(query, {})["me"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def invite(organization_id, input)
|
|
24
|
+
query = "mutation($organizationId: UUID!, $input: InviteUserInput!) { inviteUser(organizationId: $organizationId, input: $input) { #{FRAGMENT} } }"
|
|
25
|
+
@transport.execute(query, { "organizationId" => organization_id, "input" => input })["inviteUser"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def remove(id)
|
|
29
|
+
query = "mutation($id: UUID!) { removeUser(id: $id) }"
|
|
30
|
+
@transport.execute(query, { "id" => id })["removeUser"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update_role(id, input)
|
|
34
|
+
query = "mutation($id: UUID!, $input: UpdateUserRoleInput!) { updateUserRole(id: $id, input: $input) { #{FRAGMENT} } }"
|
|
35
|
+
@transport.execute(query, { "id" => id, "input" => input })["updateUserRole"]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "version"
|
|
7
|
+
|
|
8
|
+
module Hivehook
|
|
9
|
+
class GraphQLTransport
|
|
10
|
+
DEFAULT_OPEN_TIMEOUT = 10
|
|
11
|
+
DEFAULT_READ_TIMEOUT = 30
|
|
12
|
+
DEFAULT_MAX_RETRIES = 2
|
|
13
|
+
|
|
14
|
+
def initialize(base_url, api_key = nil, open_timeout: DEFAULT_OPEN_TIMEOUT,
|
|
15
|
+
read_timeout: DEFAULT_READ_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
|
|
16
|
+
@base_url = base_url
|
|
17
|
+
@api_key = api_key
|
|
18
|
+
@open_timeout = open_timeout
|
|
19
|
+
@read_timeout = read_timeout
|
|
20
|
+
@max_retries = max_retries
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def execute(query, variables = {})
|
|
24
|
+
uri = URI("#{@base_url}/graphql")
|
|
25
|
+
|
|
26
|
+
attempt = 0
|
|
27
|
+
loop do
|
|
28
|
+
response = do_request(uri, query, variables)
|
|
29
|
+
status = response.code.to_i
|
|
30
|
+
|
|
31
|
+
if status == 429
|
|
32
|
+
retry_after = parse_retry_after(response["Retry-After"])
|
|
33
|
+
if attempt < @max_retries
|
|
34
|
+
attempt += 1
|
|
35
|
+
sleep(retry_after || backoff(attempt))
|
|
36
|
+
next
|
|
37
|
+
end
|
|
38
|
+
msg = extract_message(response, "rate limited")
|
|
39
|
+
raise RateLimitError.new(msg, 429, retry_after: retry_after,
|
|
40
|
+
extensions: extract_extensions(response))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if status >= 500
|
|
44
|
+
if attempt < @max_retries
|
|
45
|
+
attempt += 1
|
|
46
|
+
sleep(backoff(attempt))
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
msg = extract_message(response, "server error")
|
|
50
|
+
raise ServerError.new(msg, status, extensions: extract_extensions(response))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return handle_response(response, status)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def do_request(uri, query, variables)
|
|
60
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
61
|
+
http.use_ssl = uri.scheme == "https"
|
|
62
|
+
http.open_timeout = @open_timeout
|
|
63
|
+
http.read_timeout = @read_timeout
|
|
64
|
+
|
|
65
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
66
|
+
request["Content-Type"] = "application/json"
|
|
67
|
+
request["User-Agent"] = "hivehook-ruby/#{Hivehook::VERSION}"
|
|
68
|
+
request["Authorization"] = "Bearer #{@api_key}" if @api_key
|
|
69
|
+
|
|
70
|
+
request.body = JSON.generate({ query: query, variables: variables })
|
|
71
|
+
http.request(request)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def handle_response(response, status)
|
|
75
|
+
if status == 401
|
|
76
|
+
msg = extract_message(response, "unauthorized")
|
|
77
|
+
raise AuthError.new(msg, 401, extensions: extract_extensions(response))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if status >= 400
|
|
81
|
+
msg = extract_message(response, response.body)
|
|
82
|
+
raise APIError.new(msg, status, extensions: extract_extensions(response))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
json = JSON.parse(response.body)
|
|
87
|
+
rescue JSON::ParserError => e
|
|
88
|
+
raise APIError.new("malformed JSON response: #{e.message}", status)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if json["errors"]&.any?
|
|
92
|
+
err = json["errors"][0]
|
|
93
|
+
ext = err["extensions"]
|
|
94
|
+
code = ext.is_a?(Hash) ? ext["code"] : nil
|
|
95
|
+
msg = err["message"]
|
|
96
|
+
|
|
97
|
+
case code
|
|
98
|
+
when "UNAUTHENTICATED", "UNAUTHORIZED"
|
|
99
|
+
raise AuthError.new(msg, 401, extensions: ext, graphql_code: code)
|
|
100
|
+
when "NOT_FOUND"
|
|
101
|
+
raise NotFoundError.new(msg, status, extensions: ext, graphql_code: code)
|
|
102
|
+
when "CONFLICT"
|
|
103
|
+
raise ConflictError.new(msg, status, extensions: ext, graphql_code: code)
|
|
104
|
+
when "VALIDATION", "BAD_USER_INPUT"
|
|
105
|
+
raise ValidationError.new(msg, status, extensions: ext, graphql_code: code)
|
|
106
|
+
else
|
|
107
|
+
raise APIError.new(msg, status, extensions: ext, graphql_code: code)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
raise APIError.new("empty response data", 500) unless json["data"]
|
|
112
|
+
|
|
113
|
+
json["data"]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def extract_message(response, fallback)
|
|
117
|
+
json = JSON.parse(response.body)
|
|
118
|
+
json.dig("errors", 0, "message") || json["message"] || fallback
|
|
119
|
+
rescue JSON::ParserError
|
|
120
|
+
fallback
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def extract_extensions(response)
|
|
124
|
+
json = JSON.parse(response.body)
|
|
125
|
+
json.dig("errors", 0, "extensions")
|
|
126
|
+
rescue JSON::ParserError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Parse Retry-After header. RFC 7231 allows seconds or HTTP-date.
|
|
131
|
+
# Returns Float seconds, or nil if unparseable.
|
|
132
|
+
def parse_retry_after(value)
|
|
133
|
+
return nil if value.nil? || value.empty?
|
|
134
|
+
if value =~ /\A\d+(\.\d+)?\z/
|
|
135
|
+
value.to_f
|
|
136
|
+
else
|
|
137
|
+
begin
|
|
138
|
+
t = Time.httpdate(value)
|
|
139
|
+
[t.to_f - Time.now.to_f, 0.0].max
|
|
140
|
+
rescue ArgumentError
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def backoff(attempt)
|
|
147
|
+
# 0.1s, 0.2s, 0.4s, ...
|
|
148
|
+
0.1 * (2**(attempt - 1))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Hivehook
|
|
6
|
+
module Webhook
|
|
7
|
+
HEADER_SIGNATURE = "X-Hivehook-Signature"
|
|
8
|
+
HEADER_TIMESTAMP = "X-Hivehook-Timestamp"
|
|
9
|
+
HEADER_MESSAGE_ID = "X-Hivehook-Message-ID"
|
|
10
|
+
|
|
11
|
+
def self.sign(payload, secret, timestamp)
|
|
12
|
+
message = "#{timestamp}.#{payload}"
|
|
13
|
+
digest = OpenSSL::HMAC.hexdigest("SHA256", secret, message)
|
|
14
|
+
"v1=#{digest}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Verify a webhook signature.
|
|
18
|
+
#
|
|
19
|
+
# tolerance_seconds semantics:
|
|
20
|
+
# nil -> skip timestamp check
|
|
21
|
+
# 0 -> strict, any drift fails
|
|
22
|
+
# positive -> allow past drift up to N seconds, reject future timestamps beyond N seconds
|
|
23
|
+
def self.verify(payload, secret, signature, timestamp, tolerance_seconds = nil)
|
|
24
|
+
unless tolerance_seconds.nil?
|
|
25
|
+
delta = Time.now.to_i - timestamp
|
|
26
|
+
# delta > 0 -> timestamp is in the past
|
|
27
|
+
# delta < 0 -> timestamp is in the future
|
|
28
|
+
if delta > tolerance_seconds || -delta > tolerance_seconds
|
|
29
|
+
return false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
v1 = extract_v1(signature)
|
|
34
|
+
return false unless v1
|
|
35
|
+
|
|
36
|
+
expected = sign(payload, secret, timestamp)
|
|
37
|
+
secure_compare(expected, v1)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.verify_with_rotation(payload, primary, secondary, signature, timestamp, tolerance_seconds = nil)
|
|
41
|
+
# Compute both verifications without short-circuiting to keep
|
|
42
|
+
# timing characteristics uniform regardless of which secret matched.
|
|
43
|
+
primary_ok = verify(payload, primary, signature, timestamp, tolerance_seconds)
|
|
44
|
+
secondary_ok = secondary ? verify(payload, secondary, signature, timestamp, tolerance_seconds) : false
|
|
45
|
+
primary_ok | secondary_ok
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Parse a multi-scheme signature header value and return the full "v1=..."
|
|
49
|
+
# element, or nil if absent. Supports comma-separated lists like
|
|
50
|
+
# "v1=abc,v2=xyz" or "t=123,v1=abc".
|
|
51
|
+
def self.extract_v1(signature)
|
|
52
|
+
return nil if signature.nil?
|
|
53
|
+
signature.split(",").map(&:strip).find { |part| part.start_with?("v1=") }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.secure_compare(a, b)
|
|
57
|
+
return false unless a.bytesize == b.bytesize
|
|
58
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private_class_method :secure_compare
|
|
62
|
+
end
|
|
63
|
+
end
|