distributed-press-api-client 0.3.1 → 0.4.0rc3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/distributed_press/v1/schemas/bittorrent_protocol.rb +1 -1
- 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 +45 -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
|