distributed-press-api-client 0.3.0 → 0.4.0rc3
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/lib/distributed_press/v1/schemas/bittorrent_protocol.rb +23 -0
- data/lib/distributed_press/v1/schemas/new_site.rb +1 -0
- data/lib/distributed_press/v1/schemas/site.rb +4 -1
- data/lib/distributed_press/v1/schemas/update_site.rb +1 -1
- data/lib/distributed_press/v1/schemas.rb +1 -0
- data/lib/distributed_press/v1/social/allowlist.rb +18 -0
- data/lib/distributed_press/v1/social/blocklist.rb +75 -0
- data/lib/distributed_press/v1/social/client.rb +220 -86
- data/lib/distributed_press/v1/social/dereferencer.rb +89 -0
- data/lib/distributed_press/v1/social/dereferencer.rb.orig +132 -0
- data/lib/distributed_press/v1/social/hook.rb +100 -0
- data/lib/distributed_press/v1/social/inbox.rb +66 -0
- data/lib/distributed_press/v1/social/outbox.rb +50 -0
- data/lib/distributed_press/v1/social/reference.rb +39 -0
- data/lib/distributed_press/v1/social/referenced_object.rb +89 -0
- data/lib/distributed_press/v1/social/schemas/webhook.rb +24 -0
- data/lib/distributed_press/v1/social/signed_headers.rb +106 -0
- data/lib/distributed_press/version.rb +1 -1
- data/lib/dry/schema/processor_decorator.rb +25 -0
- data/lib/dry/schema/result_decorator.rb +35 -0
- metadata +46 -4
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'addressable'
|
4
|
+
require_relative 'reference'
|
5
|
+
|
6
|
+
class DistributedPress
|
7
|
+
module V1
|
8
|
+
module Social
|
9
|
+
# Fetches ActivityStreams from different instances by
|
10
|
+
# instantiating clients.
|
11
|
+
class Dereferencer
|
12
|
+
# @return [DistributedPress::V1::Social::Client]
|
13
|
+
attr_reader :client
|
14
|
+
|
15
|
+
REFERENTIABLE_ATTRIBUTES =
|
16
|
+
%w[
|
17
|
+
actor
|
18
|
+
owner
|
19
|
+
attributedTo
|
20
|
+
cc
|
21
|
+
inReplyTo
|
22
|
+
object
|
23
|
+
replies
|
24
|
+
to
|
25
|
+
publicKey
|
26
|
+
|
27
|
+
alsoKnownAs
|
28
|
+
devices
|
29
|
+
featured
|
30
|
+
featuredTags
|
31
|
+
followers
|
32
|
+
following
|
33
|
+
inbox
|
34
|
+
movedTo
|
35
|
+
outbox
|
36
|
+
|
37
|
+
first
|
38
|
+
items
|
39
|
+
next
|
40
|
+
orderedItems
|
41
|
+
partOf
|
42
|
+
prev
|
43
|
+
].freeze
|
44
|
+
|
45
|
+
# @param :client [DistributedPress::V1::Social::Client]
|
46
|
+
def initialize(client:)
|
47
|
+
@client = client
|
48
|
+
@parser =
|
49
|
+
proc do |body, format|
|
50
|
+
next HTTParty::Parser.call(body, format || :plain) unless body&.starts_with? '{'
|
51
|
+
|
52
|
+
HTTParty::Parser.call(body, :json).tap do |object|
|
53
|
+
reference_object! object
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fetch a URI
|
59
|
+
#
|
60
|
+
# @param :uri [String, Addressable::URI]
|
61
|
+
# @return [HTTParty::Response]
|
62
|
+
def get(uri:)
|
63
|
+
uri = uris(uri)
|
64
|
+
|
65
|
+
clients(uri).get(endpoint: uri.path, parser: @parser)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Gets a client for a URI
|
69
|
+
#
|
70
|
+
# @param :uri [Addressable::URI]
|
71
|
+
# @return [DistributedPress::V1::Social::Client]
|
72
|
+
def clients(uri)
|
73
|
+
@clients ||= {}
|
74
|
+
@clients[uri.origin] ||=
|
75
|
+
client.class.new(
|
76
|
+
url: uri.origin,
|
77
|
+
public_key_url: client.public_key_url,
|
78
|
+
private_key_pem: client.private_key.to_s,
|
79
|
+
logger: client.logger,
|
80
|
+
cache_store: client.class.cache_store
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Gets a reference for a URI and indexes it by the complete
|
85
|
+
# and normalized URI
|
86
|
+
#
|
87
|
+
# @param :uri [String, Addressable::URI]
|
88
|
+
# @return [DistributedPress::V1::Social::Reference]
|
89
|
+
def references(uri)
|
90
|
+
@references ||= {}
|
91
|
+
@references[uri.to_s] ||= Reference.new(uri: uri.to_s, dereferencer: self)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Make sure we're getting a normalized Addressable::URI
|
95
|
+
#
|
96
|
+
# @param :uri [String, Addressable::URI]
|
97
|
+
# @return [Addressable::URI]
|
98
|
+
def uris(uri)
|
99
|
+
@uris ||= {}
|
100
|
+
@uris[uri.to_s] ||=
|
101
|
+
(if uri.is_a? Addressable::URI
|
102
|
+
uri
|
103
|
+
else
|
104
|
+
Addressable::URI.parse(uri)
|
105
|
+
end).normalize
|
106
|
+
end
|
107
|
+
|
108
|
+
def reference_object!(object)
|
109
|
+
REFERENTIABLE_ATTRIBUTES.each do |attribute|
|
110
|
+
next unless object.key? attribute
|
111
|
+
|
112
|
+
case object[attribute]
|
113
|
+
when Array
|
114
|
+
object[attribute].map! do |o|
|
115
|
+
case o
|
116
|
+
when Hash
|
117
|
+
reference_object!(o)
|
118
|
+
o
|
119
|
+
when String then references(uris(o))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
when Hash
|
123
|
+
reference_object!(object[attribute])
|
124
|
+
when String
|
125
|
+
object[attribute] = references(uris(object[attribute]))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'client'
|
4
|
+
require_relative 'schemas/webhook'
|
5
|
+
|
6
|
+
class DistributedPress
|
7
|
+
module V1
|
8
|
+
module Social
|
9
|
+
# Manages the actor's hooks on the Social Inbox
|
10
|
+
class Hook
|
11
|
+
class Error < StandardError; end
|
12
|
+
class ValidationError < Error; end
|
13
|
+
class EventNotValidError < Error; end
|
14
|
+
|
15
|
+
ACCEPT = %w[text/plain].freeze
|
16
|
+
CONTENT_TYPE = 'text/plain'
|
17
|
+
EVENTS = %w[moderationqueued onapproved onrejected].freeze
|
18
|
+
|
19
|
+
# @return [DistributedPress::V1::Social::Client]
|
20
|
+
attr_reader :client
|
21
|
+
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :actor
|
24
|
+
|
25
|
+
# @param :client [DistributedPress::V1::Social::Client]
|
26
|
+
# @param :actor [String]
|
27
|
+
def initialize(client:, actor:)
|
28
|
+
@client = client
|
29
|
+
@actor = actor
|
30
|
+
# The serializer validates the body format and raises an
|
31
|
+
# exception if it contains errors.
|
32
|
+
@serializer = proc do |body|
|
33
|
+
body.tap do |b|
|
34
|
+
next if b.errors.empty?
|
35
|
+
|
36
|
+
raise ValidationError, body.errors.to_h.map do |key, messages|
|
37
|
+
messages.map do |message|
|
38
|
+
"#{key} #{message}"
|
39
|
+
end
|
40
|
+
end.flatten.join(', ')
|
41
|
+
end.to_h.to_json
|
42
|
+
end
|
43
|
+
|
44
|
+
# XXX: format is nil but should be :json
|
45
|
+
@parser = proc do |body, format|
|
46
|
+
next HTTParty::Parser.call(body, format || :plain) unless body.starts_with? '{'
|
47
|
+
|
48
|
+
json = HTTParty::Parser.call(body, :json)
|
49
|
+
|
50
|
+
DistributedPress::V1::Social::Schemas::Webhook.new.call(json)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Gets a hook and validates return, or 404 when not found
|
55
|
+
#
|
56
|
+
# @param :event [String]
|
57
|
+
# @return [HTTParty::Response]
|
58
|
+
def get(event:)
|
59
|
+
validate_event! event
|
60
|
+
|
61
|
+
client.get(endpoint: "#{endpoint}/#{event}", parser: @parser)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Creates a webhook
|
65
|
+
#
|
66
|
+
# @param :event [String]
|
67
|
+
# @param :hook [DistributedPress::V1::Social::Schemas::Webhook]
|
68
|
+
# @return [HTTParty::Response]
|
69
|
+
def put(event:, hook:)
|
70
|
+
validate_event! event
|
71
|
+
|
72
|
+
client.put(endpoint: "#{endpoint}/#{event}", body: hook, serializer: @serializer, parser: @parser)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Removes a hook for an event
|
76
|
+
#
|
77
|
+
# @param :event [String]
|
78
|
+
# @return [HTTParty::Response]
|
79
|
+
def delete(event:)
|
80
|
+
validate_event! event
|
81
|
+
|
82
|
+
client.delete(endpoint: "#{endpoint}/#{event}", serializer: @serializer, parser: @parser)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Endpoint
|
86
|
+
#
|
87
|
+
# @return [String]
|
88
|
+
def endpoint
|
89
|
+
@endpoint ||= "/v1/#{actor}/hooks"
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def validate_event!(event)
|
95
|
+
raise EventNotValidError, "#{event} must be one of #{EVENTS.join(', ')}" unless EVENTS.include? event
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require_relative 'client'
|
5
|
+
|
6
|
+
class DistributedPress
|
7
|
+
module V1
|
8
|
+
module Social
|
9
|
+
# Manages the actor's inbox on the Social Inbox
|
10
|
+
class Inbox
|
11
|
+
# @return [DistributedPress::V1::Social::Client]
|
12
|
+
attr_reader :client
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :actor
|
16
|
+
|
17
|
+
# @param :client [DistributedPress::V1::Social::Client]
|
18
|
+
# @param :actor [String]
|
19
|
+
def initialize(client:, actor:)
|
20
|
+
@client = client
|
21
|
+
@actor = actor
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get the actor's inbox
|
25
|
+
#
|
26
|
+
# @return [HTTParty::Response]
|
27
|
+
def get
|
28
|
+
client.get(endpoint: endpoint)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Send an activity to the actor's inbox. This is typically done
|
32
|
+
# by other actors though, not ourselves, so it could be used to
|
33
|
+
# send directly to another Actor's Social Inbox.
|
34
|
+
#
|
35
|
+
# @param :activity [Hash]
|
36
|
+
# @return [HTTParty::Response]
|
37
|
+
def post(activity:)
|
38
|
+
client.post(endpoint: endpoint, body: activity)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Reject an activity queued on the inbox
|
42
|
+
#
|
43
|
+
# @param :id [String] Activity ID
|
44
|
+
# @return [HTTParty::Response]
|
45
|
+
def reject(id:)
|
46
|
+
client.delete(endpoint: "#{endpoint}/#{URI.encode_uri_component(id)}")
|
47
|
+
end
|
48
|
+
|
49
|
+
# Accept an activity queued on the inbox
|
50
|
+
#
|
51
|
+
# @param :id [String] Activity ID
|
52
|
+
# @return [HTTParty::Response]
|
53
|
+
def accept(id:)
|
54
|
+
client.post(endpoint: "#{endpoint}/#{URI.encode_uri_component(id)}", body: {})
|
55
|
+
end
|
56
|
+
|
57
|
+
# Inbox
|
58
|
+
#
|
59
|
+
# @return [String]
|
60
|
+
def endpoint
|
61
|
+
@endpoint ||= "/v1/#{actor}/inbox"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require_relative 'client'
|
5
|
+
|
6
|
+
class DistributedPress
|
7
|
+
module V1
|
8
|
+
module Social
|
9
|
+
# Manages the actor's outbox on the Social Inbox
|
10
|
+
class Outbox
|
11
|
+
# @return [DistributedPress::V1::Social::Client]
|
12
|
+
attr_reader :client
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :actor
|
16
|
+
|
17
|
+
# @param :client [DistributedPress::V1::Social::Client]
|
18
|
+
# @param :actor [String]
|
19
|
+
def initialize(client:, actor:)
|
20
|
+
@client = client
|
21
|
+
@actor = actor
|
22
|
+
end
|
23
|
+
|
24
|
+
# Send an activity to the actor's outbox. The Social Inbox will
|
25
|
+
# take care of sending it to the audiences.
|
26
|
+
#
|
27
|
+
# @param :activity [Hash]
|
28
|
+
# @return [HTTParty::Response]
|
29
|
+
def post(activity:)
|
30
|
+
client.post(endpoint: endpoint, body: activity)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get an activity queued on the outbox
|
34
|
+
#
|
35
|
+
# @param :id [String] Activity ID
|
36
|
+
# @return [HTTParty::Response]
|
37
|
+
def get(id:)
|
38
|
+
client.get(endpoint: "#{endpoint}/#{URI.encode_uri_component(id)}")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Outbox
|
42
|
+
#
|
43
|
+
# @return [String]
|
44
|
+
def endpoint
|
45
|
+
@endpoint ||= "/v1/#{actor}/outbox"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DistributedPress
|
4
|
+
module V1
|
5
|
+
module Social
|
6
|
+
# A lazy loaded reference to a remote object that can access its
|
7
|
+
# attributes directly.
|
8
|
+
class Reference
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :uri
|
13
|
+
|
14
|
+
# @return [DistributedPress::V1::Social::Dereferencer]
|
15
|
+
attr_reader :dereferencer
|
16
|
+
|
17
|
+
# @param :uri [String]
|
18
|
+
# @param :dereferencer [DistributedPress::V1::Social::Dereferencer]
|
19
|
+
def initialize(uri:, dereferencer:)
|
20
|
+
@uri = uri
|
21
|
+
@dereferencer = dereferencer
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetches the remote object once
|
25
|
+
#
|
26
|
+
# @return [HTTParty::Response]
|
27
|
+
def object
|
28
|
+
@object ||= dereferencer.get(uri: uri)
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"#{self.class.name}(#{uri})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def_delegators :object, :[], :dig, :to_h, :to_json
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DistributedPress
|
4
|
+
module V1
|
5
|
+
module Social
|
6
|
+
# An object with external references
|
7
|
+
class ReferencedObject
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
REFERENTIABLE_ATTRIBUTES =
|
11
|
+
%w[
|
12
|
+
actor
|
13
|
+
owner
|
14
|
+
attributedTo
|
15
|
+
cc
|
16
|
+
inReplyTo
|
17
|
+
object
|
18
|
+
replies
|
19
|
+
to
|
20
|
+
publicKey
|
21
|
+
|
22
|
+
alsoKnownAs
|
23
|
+
devices
|
24
|
+
featured
|
25
|
+
featuredTags
|
26
|
+
followers
|
27
|
+
following
|
28
|
+
inbox
|
29
|
+
movedTo
|
30
|
+
outbox
|
31
|
+
|
32
|
+
first
|
33
|
+
items
|
34
|
+
next
|
35
|
+
orderedItems
|
36
|
+
partOf
|
37
|
+
prev
|
38
|
+
].freeze
|
39
|
+
|
40
|
+
attr_reader :object
|
41
|
+
attr_reader :dereferencer
|
42
|
+
attr_reader :referenced
|
43
|
+
|
44
|
+
def_delegators :referenced, :[], :dig, :to_h, :to_json
|
45
|
+
|
46
|
+
def initialize(object:, dereferencer:)
|
47
|
+
@object = object
|
48
|
+
@dereferencer = dereferencer
|
49
|
+
@referenced = HTTParty::ModuleInheritableAttributes.hash_deep_dup(object)
|
50
|
+
reference_object! referenced
|
51
|
+
end
|
52
|
+
|
53
|
+
def _dump(_)
|
54
|
+
Marshal.dump([object, dereferencer])
|
55
|
+
end
|
56
|
+
|
57
|
+
def self._load(array)
|
58
|
+
object, dereferencer = Marshal.load(array)
|
59
|
+
|
60
|
+
new(object: object, dereferencer: dereferencer)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def reference_object!(object)
|
66
|
+
REFERENTIABLE_ATTRIBUTES.each do |attribute|
|
67
|
+
next unless object.key? attribute
|
68
|
+
|
69
|
+
case object[attribute]
|
70
|
+
when Array
|
71
|
+
object[attribute].map! do |o|
|
72
|
+
case o
|
73
|
+
when Hash
|
74
|
+
reference_object!(o)
|
75
|
+
o
|
76
|
+
when String then dereferencer.references(dereferencer.uris(o))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
when Hash
|
80
|
+
reference_object!(object[attribute])
|
81
|
+
when String
|
82
|
+
object[attribute] = dereferencer.references(dereferencer.uris(object[attribute]))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-schema'
|
4
|
+
require_relative '../../../../dry/schema/processor_decorator'
|
5
|
+
require_relative '../../../../dry/schema/result_decorator'
|
6
|
+
|
7
|
+
class DistributedPress
|
8
|
+
module V1
|
9
|
+
module Social
|
10
|
+
# WebhookSchema
|
11
|
+
module Schemas
|
12
|
+
class Webhook < Dry::Schema::JSON
|
13
|
+
METHODS = %w[GET POST PUT DELETE].freeze
|
14
|
+
|
15
|
+
define do
|
16
|
+
required(:url).value(:uri_rfc3986?)
|
17
|
+
required(:method).value(included_in?: METHODS)
|
18
|
+
required(:headers).hash
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DistributedPress
|
4
|
+
module V1
|
5
|
+
module Social
|
6
|
+
# Headers need to be signed by knowing the HTTP method (verb) and
|
7
|
+
# path. Since we're using caching, we need to change the
|
8
|
+
# signature after it's changed.
|
9
|
+
class SignedHeaders < Hash
|
10
|
+
# Signing algorithm
|
11
|
+
#
|
12
|
+
# @todo is it possible to use other algorithms?
|
13
|
+
ALGORITHM = 'rsa-sha256'
|
14
|
+
|
15
|
+
# Required by HTTP Signatures
|
16
|
+
REQUEST_TARGET = '(request-target)'
|
17
|
+
|
18
|
+
# Headers included in the signature
|
19
|
+
# rubocop:disable Style/MutableConstant
|
20
|
+
SIGNABLE_HEADERS = [REQUEST_TARGET, 'Host', 'Date', 'Digest']
|
21
|
+
# rubocop:enable Style/MutableConstant
|
22
|
+
|
23
|
+
# @return [HTTParty::Request]
|
24
|
+
attr_accessor :request
|
25
|
+
|
26
|
+
# @return [OpenSSL::PKey::RSA]
|
27
|
+
attr_accessor :private_key
|
28
|
+
|
29
|
+
# @return [String]
|
30
|
+
attr_accessor :public_key_url
|
31
|
+
|
32
|
+
# Takes advantage of HTTParty::Request.setup_raw_request running
|
33
|
+
# to_hash on the headers, so we sign as soon as we're ready to
|
34
|
+
# send the request.
|
35
|
+
#
|
36
|
+
# @return [Hash]
|
37
|
+
def to_hash
|
38
|
+
request_target!
|
39
|
+
sign_headers!
|
40
|
+
|
41
|
+
# xxx: converts to an actual Hash to facilitate marshaling
|
42
|
+
super.to_h
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# @return [String]
|
48
|
+
def verb
|
49
|
+
request.http_method.name.split('::').last.downcase
|
50
|
+
end
|
51
|
+
|
52
|
+
def request_target!
|
53
|
+
self[REQUEST_TARGET] = "#{verb} #{request.path}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# HTTP Signatures
|
57
|
+
#
|
58
|
+
# @see {https://docs.joinmastodon.org/spec/security/}
|
59
|
+
# @param :headers [Hash]
|
60
|
+
def sign_headers!
|
61
|
+
self['Signature'] = {
|
62
|
+
'keyId' => public_key_url,
|
63
|
+
'algorithm' => ALGORITHM,
|
64
|
+
'headers' => signable_headers,
|
65
|
+
'signature' => signed_headers
|
66
|
+
}.map do |key, value|
|
67
|
+
"#{key}=\"#{value}\""
|
68
|
+
end.join(',')
|
69
|
+
|
70
|
+
delete REQUEST_TARGET
|
71
|
+
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
# List of headers to be signed, removing headers that don't
|
76
|
+
# exist on the request.
|
77
|
+
#
|
78
|
+
# @return [String]
|
79
|
+
def signable_headers
|
80
|
+
(SIGNABLE_HEADERS & keys).join(' ').downcase
|
81
|
+
end
|
82
|
+
|
83
|
+
# Sign headers
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
def signed_headers
|
87
|
+
Base64.strict_encode64(
|
88
|
+
private_key.sign(
|
89
|
+
OpenSSL::Digest.new('SHA256'),
|
90
|
+
signature_content
|
91
|
+
)
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Generates a string to be signed
|
96
|
+
#
|
97
|
+
# @return [String]
|
98
|
+
def signature_content
|
99
|
+
slice(*SIGNABLE_HEADERS).map do |key, value|
|
100
|
+
"#{key.downcase}: #{value}"
|
101
|
+
end.join("\n")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/schema/processor'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# Include the processor as Result option so we can deserialize
|
8
|
+
# later.
|
9
|
+
module ProcessorDecorator
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
# Add the processor to the result so we can deserialize it
|
13
|
+
# later
|
14
|
+
def call(input)
|
15
|
+
Result.new(input.dup, message_compiler: message_compiler, processor: self) do |result|
|
16
|
+
steps.call(result)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Dry::Schema::Processor.include Dry::Schema::ProcessorDecorator
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/schema/processor'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# This decorator allows to serialize Dry::Schema::Result objects
|
8
|
+
# by storing the data and processor so they can be run again during
|
9
|
+
# deserialization.
|
10
|
+
module ResultDecorator
|
11
|
+
def self.included(base)
|
12
|
+
base.class_eval do
|
13
|
+
option :processor
|
14
|
+
|
15
|
+
def _dump(_)
|
16
|
+
Marshal.dump(
|
17
|
+
{
|
18
|
+
processor: processor.class,
|
19
|
+
data: to_h
|
20
|
+
}
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self._load(data)
|
25
|
+
data = Marshal.load(data)
|
26
|
+
|
27
|
+
data[:processor].new.call(data[:data])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Dry::Schema::Result.include Dry::Schema::ResultDecorator
|