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.
@@ -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
- # Name reserved for Unitpost (https://unitpost.com).
2
- # Placeholder gem; the official Unitpost Ruby SDK will be published under this name.
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.0.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-06-26 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Intentionally empty placeholder. The official Unitpost Ruby SDK will
14
- be published under this name.
15
- email:
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
- homepage: https://unitpost.com
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
- post_install_message:
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: '2.7'
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.0.3.1
44
- signing_key:
101
+ rubygems_version: 3.4.19
102
+ signing_key:
45
103
  specification_version: 4
46
- summary: Name reserved for Unitpost. Official Ruby SDK will be published here.
104
+ summary: Official Unitpost SDK for Ruby.
47
105
  test_files: []