raims 0.1.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.
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Raims
6
+ class Webhook
7
+ ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
8
+
9
+ def self.verify_signature(payload, headers, secret, tolerance: 300, now: nil)
10
+ raise ArgumentError, 'Webhook tolerance must be a non-negative integer.' unless tolerance.is_a?(Integer) && tolerance >= 0
11
+ current = now.nil? ? Time.now.to_i : now
12
+ raise ArgumentError, 'Webhook now must be a non-negative integer.' unless current.is_a?(Integer) && current >= 0
13
+ raise SignatureVerificationError, 'The webhook body must contain the original HTTP bytes.' unless payload.is_a?(String)
14
+ delivery = header(headers, 'webhook-id')
15
+ timestamp = header(headers, 'webhook-timestamp')
16
+ signatures = header(headers, 'webhook-signature')
17
+ unless delivery.match?(/\A[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\z/)
18
+ raise SignatureVerificationError, 'The webhook delivery ID is invalid.'
19
+ end
20
+ unless timestamp.match?(/\A[0-9]{1,12}\z/) && (timestamp.to_i - current).abs <= tolerance
21
+ raise SignatureVerificationError, 'The webhook timestamp is invalid or outside tolerance.'
22
+ end
23
+ key = decode_secret(secret)
24
+ expected = OpenSSL::HMAC.digest('SHA256', key, "#{delivery}.#{timestamp}.".b + payload.b)
25
+ matched = false
26
+ signatures.split(',').each do |candidate|
27
+ match = candidate.strip.match(/\Av1=([0-9a-fA-F]{64})\z/i)
28
+ next unless match
29
+ valid = OpenSSL.fixed_length_secure_compare(expected, [match[1]].pack('H*'))
30
+ matched = true if valid
31
+ end
32
+ raise SignatureVerificationError, 'No webhook signature matches the configured secret.' unless matched
33
+ nil
34
+ end
35
+
36
+ def self.construct_event(payload, headers, secret, tolerance: 300, now: nil)
37
+ verify_signature(payload, headers, secret, tolerance: tolerance, now: now)
38
+ begin
39
+ event = Util.parse_json(payload)
40
+ rescue JSON::ParserError, ArgumentError
41
+ raise SignatureVerificationError, 'The authenticated webhook body is not valid JSON.', cause: nil
42
+ end
43
+ raise SignatureVerificationError, 'The authenticated webhook body must be a JSON object.' unless event.is_a?(Hash)
44
+ event
45
+ end
46
+
47
+ def self.header(headers, name)
48
+ raise SignatureVerificationError, 'Webhook headers must be a Hash.' unless headers.is_a?(Hash)
49
+ values = headers.select { |key, _value| key.is_a?(String) && key.downcase == name }.values
50
+ raise SignatureVerificationError, 'A webhook signing header is missing or duplicated.' unless values.length == 1
51
+ value = values.first
52
+ value = value.first if value.is_a?(Array) && value.length == 1
53
+ unless value.is_a?(String) && value.ascii_only? && !value.empty? && !value.match?(/[\x00-\x08\x0a-\x1f\x7f]/)
54
+ raise SignatureVerificationError, 'A webhook signing header is invalid.'
55
+ end
56
+ value
57
+ end
58
+ private_class_method :header
59
+
60
+ def self.decode_secret(secret)
61
+ unless secret.is_a?(String) && secret.ascii_only? && secret.match?(/\Awhsec_[0-9A-Za-z]{1,43}\z/)
62
+ raise SignatureVerificationError, 'The webhook signing secret is invalid.'
63
+ end
64
+ encoded = secret.delete_prefix('whsec_')
65
+ raise SignatureVerificationError, 'The webhook signing secret is invalid.' if encoded.length > 1 && encoded.start_with?('0')
66
+ number = encoded.each_char.reduce(0) { |value, char| value * 62 + ALPHABET.index(char) }
67
+ raise SignatureVerificationError, 'The webhook signing secret is invalid.' if number >= (1 << 256)
68
+ Array.new(32) { |index| (number >> (index * 8)) & 255 }.pack('C*')
69
+ end
70
+ private_class_method :decode_secret
71
+ end
72
+ end
data/lib/raims.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'raims/version'
4
+ require_relative 'raims/util'
5
+ require_relative 'raims/api_response'
6
+ require_relative 'raims/errors'
7
+ require_relative 'raims/request_options'
8
+ require_relative 'raims/api_resource'
9
+ require_relative 'raims/models'
10
+ require_relative 'raims/enum_values'
11
+ require_relative 'raims/schema'
12
+ require_relative 'raims/api_list'
13
+ require_relative 'raims/http_client'
14
+ require_relative 'raims/services'
15
+ require_relative 'raims/raims_client'
16
+ require_relative 'raims/webhook'
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: raims
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Raims
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: net-http
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0.4'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0.4'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '1'
32
+ - !ruby/object:Gem::Dependency
33
+ name: json
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 2.19.9
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '3'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 2.19.9
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '3'
52
+ - !ruby/object:Gem::Dependency
53
+ name: bigdecimal
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '3.1'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '5'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '5'
72
+ - !ruby/object:Gem::Dependency
73
+ name: openssl
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '3.2'
79
+ - - "<"
80
+ - !ruby/object:Gem::Version
81
+ version: '5'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '3.2'
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '5'
92
+ description: Use the Raims public API from Ruby applications with resource services,
93
+ cursor pagination, structured errors, and webhook signature verification.
94
+ email:
95
+ - security@raims.com
96
+ executables: []
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - LICENSE
101
+ - README.md
102
+ - lib/raims.rb
103
+ - lib/raims/api_list.rb
104
+ - lib/raims/api_resource.rb
105
+ - lib/raims/api_response.rb
106
+ - lib/raims/enum_values.rb
107
+ - lib/raims/errors.rb
108
+ - lib/raims/generated/contract.json
109
+ - lib/raims/http_client.rb
110
+ - lib/raims/models.rb
111
+ - lib/raims/raims_client.rb
112
+ - lib/raims/request_options.rb
113
+ - lib/raims/schema.rb
114
+ - lib/raims/services.rb
115
+ - lib/raims/util.rb
116
+ - lib/raims/version.rb
117
+ - lib/raims/webhook.rb
118
+ homepage: https://docs.raims.com/en/integracoes/sdk-ruby
119
+ licenses:
120
+ - MIT
121
+ metadata:
122
+ source_code_uri: https://github.com/raimshq/raims-ruby
123
+ documentation_uri: https://docs.raims.com/en/integracoes/sdk-ruby
124
+ bug_tracker_uri: https://github.com/raimshq/raims-ruby/issues
125
+ rubygems_mfa_required: 'true'
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '3.3'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubygems_version: 4.0.20
141
+ specification_version: 4
142
+ summary: Official Ruby SDK for the Raims public API.
143
+ test_files: []