unitpost 0.0.0 → 0.2.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 +160 -3
- data/lib/unitpost/client.rb +49 -0
- data/lib/unitpost/errors.rb +29 -0
- data/lib/unitpost/generated/operations.rb +1121 -0
- data/lib/unitpost/http_client.rb +160 -0
- data/lib/unitpost/resources.rb +474 -0
- data/lib/unitpost/version.rb +3 -1
- data/lib/unitpost/webhooks.rb +76 -0
- data/lib/unitpost.rb +10 -2
- metadata +72 -14
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
|
|
8
|
+
module Unitpost
|
|
9
|
+
# Webhook signature verification (svix-compatible). Mirrors the engine signer:
|
|
10
|
+
# signed content: "<svix-id>.<svix-timestamp>.<raw-body>"
|
|
11
|
+
# signature: base64( HMAC_SHA256(secret_bytes, signed_content) )
|
|
12
|
+
# header: svix-signature = "v1,<base64>" (space-separated list allowed)
|
|
13
|
+
# secret: "whsec_<base64url>" — bytes after the prefix are the key.
|
|
14
|
+
#
|
|
15
|
+
# Verify the RAW request body string (not a re-serialized hash).
|
|
16
|
+
module Webhooks
|
|
17
|
+
SECRET_PREFIX = "whsec_"
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def verify(payload:, secret:, headers:, tolerance_seconds: 300)
|
|
22
|
+
msg_id = header(headers, "svix-id")
|
|
23
|
+
timestamp = header(headers, "svix-timestamp")
|
|
24
|
+
signature = header(headers, "svix-signature")
|
|
25
|
+
|
|
26
|
+
if msg_id.nil? || timestamp.nil? || signature.nil?
|
|
27
|
+
raise WebhookVerificationError, "Missing svix-id, svix-timestamp, or svix-signature header."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
ts = Integer(timestamp, exception: false)
|
|
31
|
+
raise WebhookVerificationError, "Invalid svix-timestamp header." if ts.nil?
|
|
32
|
+
|
|
33
|
+
now = Time.now.to_i
|
|
34
|
+
if (now - ts).abs > tolerance_seconds
|
|
35
|
+
raise WebhookVerificationError, "Webhook timestamp is outside the tolerance window (possible replay)."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
signed_content = "#{msg_id}.#{ts}.#{payload}"
|
|
39
|
+
expected = Base64.strict_encode64(
|
|
40
|
+
OpenSSL::HMAC.digest("SHA256", secret_key_bytes(secret), signed_content)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
matched = signature.split(" ").any? do |part|
|
|
44
|
+
_, sig = part.split(",", 2)
|
|
45
|
+
sig && secure_compare(sig, expected)
|
|
46
|
+
end
|
|
47
|
+
raise WebhookVerificationError, "Webhook signature verification failed." unless matched
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
JSON.parse(payload)
|
|
51
|
+
rescue JSON::ParserError
|
|
52
|
+
raise WebhookVerificationError, "Webhook payload is not valid JSON."
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def header(headers, name)
|
|
57
|
+
headers[name] || headers[name.downcase] || headers[name.split("-").map(&:capitalize).join("-")]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def secret_key_bytes(secret)
|
|
61
|
+
raw = secret.start_with?(SECRET_PREFIX) ? secret[SECRET_PREFIX.length..] : secret
|
|
62
|
+
Base64.urlsafe_decode64(raw + ("=" * ((4 - (raw.length % 4)) % 4)))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def secure_compare(a, b)
|
|
66
|
+
return false unless a.bytesize == b.bytesize
|
|
67
|
+
|
|
68
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
# Fallback for older OpenSSL without fixed_length_secure_compare.
|
|
71
|
+
res = 0
|
|
72
|
+
a.bytes.zip(b.bytes) { |x, y| res |= x ^ y }
|
|
73
|
+
res.zero?
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/unitpost.rb
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
3
|
require_relative "unitpost/version"
|
|
4
|
+
require_relative "unitpost/errors"
|
|
5
|
+
require_relative "unitpost/client"
|
|
6
|
+
require_relative "unitpost/webhooks"
|
|
4
7
|
|
|
8
|
+
# unitpost — Official Unitpost SDK for Ruby.
|
|
5
9
|
module Unitpost
|
|
10
|
+
# Convenience: Unitpost.verify_webhook(...) delegates to Unitpost::Webhooks.verify.
|
|
11
|
+
def self.verify_webhook(payload:, secret:, headers:, tolerance_seconds: 300)
|
|
12
|
+
Webhooks.verify(payload: payload, secret: secret, headers: headers, tolerance_seconds: tolerance_seconds)
|
|
13
|
+
end
|
|
6
14
|
end
|
metadata
CHANGED
|
@@ -1,31 +1,89 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: unitpost
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Unitpost
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
12
|
-
dependencies:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
date: 2026-07-31 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '2.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: rspec
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
type: :development
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.12'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: webmock
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.19'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.19'
|
|
61
|
+
description: Send email and manage contacts, segments, campaigns, templates, brand
|
|
62
|
+
kits, domains, suppressions, webhooks, and usage with the Unitpost API.
|
|
63
|
+
email:
|
|
16
64
|
executables: []
|
|
17
65
|
extensions: []
|
|
18
66
|
extra_rdoc_files: []
|
|
19
67
|
files:
|
|
20
68
|
- README.md
|
|
21
69
|
- lib/unitpost.rb
|
|
70
|
+
- lib/unitpost/client.rb
|
|
71
|
+
- lib/unitpost/errors.rb
|
|
72
|
+
- lib/unitpost/generated/operations.rb
|
|
73
|
+
- lib/unitpost/http_client.rb
|
|
74
|
+
- lib/unitpost/resources.rb
|
|
22
75
|
- lib/unitpost/version.rb
|
|
23
|
-
|
|
76
|
+
- lib/unitpost/webhooks.rb
|
|
77
|
+
homepage: https://www.unitpost.com
|
|
24
78
|
licenses:
|
|
25
79
|
- MIT
|
|
26
80
|
metadata:
|
|
27
|
-
homepage_uri: https://unitpost.com
|
|
28
|
-
|
|
81
|
+
homepage_uri: https://www.unitpost.com
|
|
82
|
+
documentation_uri: https://www.unitpost.com/docs
|
|
83
|
+
source_code_uri: https://github.com/unitpostcom/unitpost
|
|
84
|
+
bug_tracker_uri: https://github.com/unitpostcom/unitpost/issues
|
|
85
|
+
rubygems_mfa_required: 'true'
|
|
86
|
+
post_install_message:
|
|
29
87
|
rdoc_options: []
|
|
30
88
|
require_paths:
|
|
31
89
|
- lib
|
|
@@ -33,15 +91,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
33
91
|
requirements:
|
|
34
92
|
- - ">="
|
|
35
93
|
- !ruby/object:Gem::Version
|
|
36
|
-
version: '
|
|
94
|
+
version: '3.0'
|
|
37
95
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
38
96
|
requirements:
|
|
39
97
|
- - ">="
|
|
40
98
|
- !ruby/object:Gem::Version
|
|
41
99
|
version: '0'
|
|
42
100
|
requirements: []
|
|
43
|
-
rubygems_version: 3.
|
|
44
|
-
signing_key:
|
|
101
|
+
rubygems_version: 3.4.19
|
|
102
|
+
signing_key:
|
|
45
103
|
specification_version: 4
|
|
46
|
-
summary:
|
|
104
|
+
summary: Official Unitpost SDK for Ruby.
|
|
47
105
|
test_files: []
|