telnyx 5.38.0 → 5.38.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da4cb6122eefdc8ddf57152d47edd56e0983f3d228b2db64f57f3c33cf1daa43
4
- data.tar.gz: 2258bab28da996a1a223c9bab28705e5260c968383652821bc2d719cfb6f67d5
3
+ metadata.gz: 18f02735965f6afbffaec63905bdc5d5b951c5cac8a5eb4ec550c1777933d485
4
+ data.tar.gz: 23493c86e9635ca193f9b0b6aac0b97c5003907bcc559108bdc0f544529516e7
5
5
  SHA512:
6
- metadata.gz: f778d0c0d0d01ef041ddbcb287603507eb1e3d6618f89c006c1e5365d5a88bc6c5d827b0449bfae14b2e6bea0fc190147a7a609ba915a1cd0501a510e31ac1cc
7
- data.tar.gz: 60099f06f8bb50305fd0944057541436883567011c0a8fbe0900ec82a95338b7120d415dc910d64bf754504b58cb795426b520ebba998641e33a2fdb5460f25c
6
+ metadata.gz: 7d456f5bc0c2a1cbc4a15cb6b434234894539c6e3327d15b35159decc0797d752b4381147521714d9031dd4b2941618fc03c7b079eb8ad4270b05381747c5721
7
+ data.tar.gz: 9a85318ee403c82111ced92fdcc0dee90a4ee266c9b657b52ed7b7270405408d06b5f74438861bedd29c47c76d0c099efb277434c02ebeb369ba52ad8d77fc73
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.38.1 (2026-02-24)
4
+
5
+ Full Changelog: [v5.38.0...v5.38.1](https://github.com/team-telnyx/telnyx-ruby/compare/v5.38.0...v5.38.1)
6
+
3
7
  ## 5.38.0 (2026-02-24)
4
8
 
5
9
  Full Changelog: [v5.37.0...v5.38.0](https://github.com/team-telnyx/telnyx-ruby/compare/v5.37.0...v5.38.0)
data/README.md CHANGED
@@ -24,7 +24,7 @@ To use this gem, install via Bundler by adding the following to your application
24
24
  <!-- x-release-please-start-version -->
25
25
 
26
26
  ```ruby
27
- gem "telnyx", "~> 5.38.0"
27
+ gem "telnyx", "~> 5.38.1"
28
28
  ```
29
29
 
30
30
  <!-- x-release-please-end -->
data/lib/telnyx/errors.rb CHANGED
@@ -224,5 +224,15 @@ module Telnyx
224
224
  class InternalServerError < Telnyx::Errors::APIStatusError
225
225
  HTTP_STATUS = (500..)
226
226
  end
227
+
228
+ # Raised when webhook signature verification fails.
229
+ class WebhookVerificationError < Telnyx::Errors::Error
230
+ # @api private
231
+ #
232
+ # @param message [String]
233
+ def initialize(message:)
234
+ super(message)
235
+ end
236
+ end
227
237
  end
228
238
  end
@@ -1,30 +1,213 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
4
+ require "json"
5
+ require "openssl"
6
+
3
7
  module Telnyx
4
8
  module Resources
9
+ # Telnyx webhook verification using ED25519 signatures.
10
+ #
11
+ # This class provides ED25519 signature verification for Telnyx webhooks,
12
+ # matching the implementation pattern used in the Python and Node SDKs.
13
+ #
14
+ # Example usage:
15
+ #
16
+ # client = Telnyx::Client.new(
17
+ # api_key: ENV["TELNYX_API_KEY"],
18
+ # public_key: ENV["TELNYX_PUBLIC_KEY"] # Base64 from Mission Control
19
+ # )
20
+ #
21
+ # # In your webhook handler:
22
+ # event = client.webhooks.unwrap(payload, headers)
23
+ #
5
24
  class Webhooks
25
+ # Telnyx webhook signature headers (case-insensitive per HTTP spec)
26
+ SIGNATURE_HEADER = "telnyx-signature-ed25519"
27
+ TIMESTAMP_HEADER = "telnyx-timestamp"
28
+
29
+ # Tolerance for timestamp validation (5 minutes)
30
+ TIMESTAMP_TOLERANCE_SECONDS = 300
31
+
32
+ # Unwraps a webhook event from its JSON representation without verifying the signature.
33
+ #
6
34
  # @param payload [String] The raw webhook payload as a string
7
35
  #
8
- # @return [Telnyx::Models::CallAIGatherEndedWebhookEvent, Telnyx::Models::CallAIGatherMessageHistoryUpdatedWebhookEvent, Telnyx::Models::CallAIGatherPartialResultsWebhookEvent, Telnyx::Models::CallAnsweredWebhookEvent, Telnyx::Models::CallBridgedWebhookEvent, Telnyx::Models::CallConversationEndedWebhookEvent, Telnyx::Models::CallConversationInsightsGeneratedWebhookEvent, Telnyx::Models::CallDtmfReceivedWebhookEvent, Telnyx::Models::CallEnqueuedWebhookEvent, Telnyx::Models::CallForkStartedWebhookEvent, Telnyx::Models::CallForkStoppedWebhookEvent, Telnyx::Models::CallGatherEndedWebhookEvent, Telnyx::Models::CallHangupWebhookEvent, Telnyx::Models::CallInitiatedWebhookEvent, Telnyx::Models::CallLeftQueueWebhookEvent, Telnyx::Models::CallMachineDetectionEndedWebhookEvent, Telnyx::Models::CallMachineGreetingEndedWebhookEvent, Telnyx::Models::CallMachinePremiumDetectionEndedWebhookEvent, Telnyx::Models::CallMachinePremiumGreetingEndedWebhookEvent, Telnyx::Models::CallPlaybackEndedWebhookEvent, Telnyx::Models::CallPlaybackStartedWebhookEvent, Telnyx::Models::CallRecordingErrorWebhookEvent, Telnyx::Models::CallRecordingSavedWebhookEvent, Telnyx::Models::CallRecordingTranscriptionSavedWebhookEvent, Telnyx::Models::CallReferCompletedWebhookEvent, Telnyx::Models::CallReferFailedWebhookEvent, Telnyx::Models::CallReferStartedWebhookEvent, Telnyx::Models::CallSiprecFailedWebhookEvent, Telnyx::Models::CallSiprecStartedWebhookEvent, Telnyx::Models::CallSiprecStoppedWebhookEvent, Telnyx::Models::CallSpeakEndedWebhookEvent, Telnyx::Models::CallSpeakStartedWebhookEvent, Telnyx::Models::CallStreamingFailedWebhookEvent, Telnyx::Models::CallStreamingStartedWebhookEvent, Telnyx::Models::CallStreamingStoppedWebhookEvent, Telnyx::Models::CampaignStatusUpdate, Telnyx::Models::ConferenceCreatedWebhookEvent, Telnyx::Models::ConferenceEndedWebhookEvent, Telnyx::Models::ConferenceFloorChanged, Telnyx::Models::ConferenceParticipantJoinedWebhookEvent, Telnyx::Models::ConferenceParticipantLeftWebhookEvent, Telnyx::Models::ConferenceParticipantPlaybackEndedWebhookEvent, Telnyx::Models::ConferenceParticipantPlaybackStartedWebhookEvent, Telnyx::Models::ConferenceParticipantSpeakEndedWebhookEvent, Telnyx::Models::ConferenceParticipantSpeakStartedWebhookEvent, Telnyx::Models::ConferencePlaybackEndedWebhookEvent, Telnyx::Models::ConferencePlaybackStartedWebhookEvent, Telnyx::Models::ConferenceRecordingSavedWebhookEvent, Telnyx::Models::ConferenceSpeakEndedWebhookEvent, Telnyx::Models::ConferenceSpeakStartedWebhookEvent, Telnyx::Models::DeliveryUpdateWebhookEvent, Telnyx::Models::FaxDelivered, Telnyx::Models::FaxFailed, Telnyx::Models::FaxMediaProcessed, Telnyx::Models::FaxQueued, Telnyx::Models::FaxSendingStarted, Telnyx::Models::InboundMessageWebhookEvent, Telnyx::Models::NumberOrderStatusUpdate, Telnyx::Models::ReplacedLinkClickWebhookEvent, Telnyx::Models::TranscriptionWebhookEvent]
36
+ # @return [Telnyx::Models::UnsafeUnwrapWebhookEvent]
9
37
  def unsafe_unwrap(payload)
10
38
  parsed = JSON.parse(payload, symbolize_names: true)
11
39
  Telnyx::Internal::Type::Converter.coerce(Telnyx::Models::UnsafeUnwrapWebhookEvent, parsed)
12
40
  end
13
41
 
42
+ # Unwraps a webhook event from its JSON representation, verifying the signature if headers are provided.
43
+ #
44
+ # When headers are provided and the client has a public_key configured, this method will verify
45
+ # the ED25519 signature to ensure the webhook came from Telnyx.
46
+ #
14
47
  # @param payload [String] The raw webhook payload as a string
48
+ # @param headers [Hash, nil] Optional HTTP headers from the webhook request
49
+ # @param key [String, nil] Optional public key override (base64-encoded)
50
+ #
51
+ # @return [Telnyx::Models::UnwrapWebhookEvent]
15
52
  #
16
- # @return [Telnyx::Models::CallAIGatherEndedWebhookEvent, Telnyx::Models::CallAIGatherMessageHistoryUpdatedWebhookEvent, Telnyx::Models::CallAIGatherPartialResultsWebhookEvent, Telnyx::Models::CallAnsweredWebhookEvent, Telnyx::Models::CallBridgedWebhookEvent, Telnyx::Models::CallConversationEndedWebhookEvent, Telnyx::Models::CallConversationInsightsGeneratedWebhookEvent, Telnyx::Models::CallDtmfReceivedWebhookEvent, Telnyx::Models::CallEnqueuedWebhookEvent, Telnyx::Models::CallForkStartedWebhookEvent, Telnyx::Models::CallForkStoppedWebhookEvent, Telnyx::Models::CallGatherEndedWebhookEvent, Telnyx::Models::CallHangupWebhookEvent, Telnyx::Models::CallInitiatedWebhookEvent, Telnyx::Models::CallLeftQueueWebhookEvent, Telnyx::Models::CallMachineDetectionEndedWebhookEvent, Telnyx::Models::CallMachineGreetingEndedWebhookEvent, Telnyx::Models::CallMachinePremiumDetectionEndedWebhookEvent, Telnyx::Models::CallMachinePremiumGreetingEndedWebhookEvent, Telnyx::Models::CallPlaybackEndedWebhookEvent, Telnyx::Models::CallPlaybackStartedWebhookEvent, Telnyx::Models::CallRecordingErrorWebhookEvent, Telnyx::Models::CallRecordingSavedWebhookEvent, Telnyx::Models::CallRecordingTranscriptionSavedWebhookEvent, Telnyx::Models::CallReferCompletedWebhookEvent, Telnyx::Models::CallReferFailedWebhookEvent, Telnyx::Models::CallReferStartedWebhookEvent, Telnyx::Models::CallSiprecFailedWebhookEvent, Telnyx::Models::CallSiprecStartedWebhookEvent, Telnyx::Models::CallSiprecStoppedWebhookEvent, Telnyx::Models::CallSpeakEndedWebhookEvent, Telnyx::Models::CallSpeakStartedWebhookEvent, Telnyx::Models::CallStreamingFailedWebhookEvent, Telnyx::Models::CallStreamingStartedWebhookEvent, Telnyx::Models::CallStreamingStoppedWebhookEvent, Telnyx::Models::CampaignStatusUpdate, Telnyx::Models::ConferenceCreatedWebhookEvent, Telnyx::Models::ConferenceEndedWebhookEvent, Telnyx::Models::ConferenceFloorChanged, Telnyx::Models::ConferenceParticipantJoinedWebhookEvent, Telnyx::Models::ConferenceParticipantLeftWebhookEvent, Telnyx::Models::ConferenceParticipantPlaybackEndedWebhookEvent, Telnyx::Models::ConferenceParticipantPlaybackStartedWebhookEvent, Telnyx::Models::ConferenceParticipantSpeakEndedWebhookEvent, Telnyx::Models::ConferenceParticipantSpeakStartedWebhookEvent, Telnyx::Models::ConferencePlaybackEndedWebhookEvent, Telnyx::Models::ConferencePlaybackStartedWebhookEvent, Telnyx::Models::ConferenceRecordingSavedWebhookEvent, Telnyx::Models::ConferenceSpeakEndedWebhookEvent, Telnyx::Models::ConferenceSpeakStartedWebhookEvent, Telnyx::Models::DeliveryUpdateWebhookEvent, Telnyx::Models::FaxDelivered, Telnyx::Models::FaxFailed, Telnyx::Models::FaxMediaProcessed, Telnyx::Models::FaxQueued, Telnyx::Models::FaxSendingStarted, Telnyx::Models::InboundMessageWebhookEvent, Telnyx::Models::NumberOrderStatusUpdate, Telnyx::Models::ReplacedLinkClickWebhookEvent, Telnyx::Models::TranscriptionWebhookEvent]
17
- def unwrap(payload)
53
+ # @raise [Telnyx::Errors::WebhookVerificationError] If signature verification fails
54
+ def unwrap(payload, headers = nil, key: nil)
55
+ # Get public key from argument or client
56
+ public_key = key || @client.public_key
57
+
58
+ # If we have headers and a public key, verify the signature
59
+ if headers && !headers.empty? && public_key && !public_key.empty?
60
+ verify_signature(payload, headers, public_key)
61
+ end
62
+
18
63
  parsed = JSON.parse(payload, symbolize_names: true)
19
64
  Telnyx::Internal::Type::Converter.coerce(Telnyx::Models::UnwrapWebhookEvent, parsed)
20
65
  end
21
66
 
67
+ # Verify webhook signature without parsing the payload.
68
+ #
69
+ # This method is consistent with the Node SDK's verify() method, allowing
70
+ # signature verification without parsing the webhook payload. The bang (!)
71
+ # indicates this method raises an exception on failure (Ruby convention).
72
+ #
73
+ # @param payload [String] The raw webhook payload
74
+ # @param headers [Hash] The webhook headers
75
+ # @param key [String, nil] Optional public key override (base64-encoded)
76
+ #
77
+ # @return [void]
78
+ #
79
+ # @raise [Telnyx::Errors::WebhookVerificationError] If verification fails or no public key available
80
+ def verify!(payload, headers, key: nil)
81
+ public_key = key || @client.public_key
82
+
83
+ unless public_key && !public_key.empty?
84
+ raise Telnyx::Errors::WebhookVerificationError.new(
85
+ message: "No public key configured. Provide key parameter or configure client with public_key."
86
+ )
87
+ end
88
+
89
+ verify_signature(payload, headers, public_key)
90
+ end
91
+
22
92
  # @api private
23
93
  #
24
94
  # @param client [Telnyx::Client]
25
95
  def initialize(client:)
26
96
  @client = client
27
97
  end
98
+
99
+ private
100
+
101
+ # Get header value case-insensitively
102
+ #
103
+ # @param headers [Hash] The headers hash
104
+ # @param key [String] The header key to find
105
+ #
106
+ # @return [String, nil]
107
+ def get_header(headers, key)
108
+ key_lower = key.downcase
109
+ headers.each do |header_key, header_value|
110
+ return header_value if header_key.to_s.downcase == key_lower
111
+ end
112
+ nil
113
+ end
114
+
115
+ # Verify the webhook signature using ED25519 cryptography
116
+ #
117
+ # @param payload [String] The raw webhook payload
118
+ # @param headers [Hash] The webhook headers
119
+ # @param public_key [String] The ED25519 public key (base64-encoded)
120
+ #
121
+ # @raise [Telnyx::Errors::WebhookVerificationError] If verification fails
122
+ def verify_signature(payload, headers, public_key)
123
+ # Extract required headers (case-insensitive)
124
+ signature_header = get_header(headers, SIGNATURE_HEADER)
125
+ timestamp_header = get_header(headers, TIMESTAMP_HEADER)
126
+
127
+ unless signature_header
128
+ raise Telnyx::Errors::WebhookVerificationError.new(
129
+ message: "Missing required header: #{SIGNATURE_HEADER}"
130
+ )
131
+ end
132
+
133
+ unless timestamp_header
134
+ raise Telnyx::Errors::WebhookVerificationError.new(
135
+ message: "Missing required header: #{TIMESTAMP_HEADER}"
136
+ )
137
+ end
138
+
139
+ # Validate timestamp to prevent replay attacks (5 minute tolerance)
140
+ begin
141
+ webhook_time = Integer(timestamp_header)
142
+ current_time = Time.now.to_i
143
+ time_diff = (current_time - webhook_time).abs
144
+
145
+ if time_diff > TIMESTAMP_TOLERANCE_SECONDS
146
+ raise Telnyx::Errors::WebhookVerificationError.new(
147
+ message: "Webhook timestamp is too old or too new (#{time_diff}s difference)"
148
+ )
149
+ end
150
+ rescue ArgumentError
151
+ raise Telnyx::Errors::WebhookVerificationError.new(
152
+ message: "Invalid timestamp format: #{timestamp_header}"
153
+ )
154
+ end
155
+
156
+ # Decode public key from base64
157
+ begin
158
+ public_key_bytes = Base64.strict_decode64(public_key)
159
+
160
+ if public_key_bytes.bytesize != 32
161
+ raise Telnyx::Errors::WebhookVerificationError.new(
162
+ message: "Invalid public key: expected 32 bytes, got #{public_key_bytes.bytesize} bytes"
163
+ )
164
+ end
165
+ rescue ArgumentError => e
166
+ raise Telnyx::Errors::WebhookVerificationError.new(
167
+ message: "Invalid public key format: #{e.message}"
168
+ )
169
+ end
170
+
171
+ # Decode signature from base64
172
+ begin
173
+ signature_bytes = Base64.strict_decode64(signature_header)
174
+
175
+ if signature_bytes.bytesize != 64
176
+ raise Telnyx::Errors::WebhookVerificationError.new(
177
+ message: "Invalid signature length: expected 64 bytes, got #{signature_bytes.bytesize} bytes"
178
+ )
179
+ end
180
+ rescue ArgumentError => e
181
+ raise Telnyx::Errors::WebhookVerificationError.new(
182
+ message: "Invalid signature format: #{e.message}"
183
+ )
184
+ end
185
+
186
+ # Create the signed payload: timestamp|payload
187
+ signed_payload = "#{timestamp_header}|#{payload}"
188
+
189
+ # Build ED25519 public key for verification
190
+ # The raw 32-byte key needs to be wrapped in X.509 SubjectPublicKeyInfo format
191
+ # ED25519 OID: 1.3.101.112 = 06 03 2b 65 70
192
+ # X.509 SPKI header for ED25519: 30 2a 30 05 06 03 2b 65 70 03 21 00
193
+ ed25519_spki_header = [0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00].pack("C*")
194
+ der_key = ed25519_spki_header + public_key_bytes
195
+
196
+ begin
197
+ pkey = OpenSSL::PKey.read(der_key)
198
+ valid = pkey.verify(nil, signature_bytes, signed_payload)
199
+
200
+ unless valid
201
+ raise Telnyx::Errors::WebhookVerificationError.new(
202
+ message: "Signature verification failed: signature does not match payload"
203
+ )
204
+ end
205
+ rescue OpenSSL::PKey::PKeyError => e
206
+ raise Telnyx::Errors::WebhookVerificationError.new(
207
+ message: "Signature verification failed: #{e.message}"
208
+ )
209
+ end
210
+ end
28
211
  end
29
212
  end
30
213
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Telnyx
4
- VERSION = "5.38.0"
4
+ VERSION = "5.38.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telnyx
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.38.0
4
+ version: 5.38.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Telnyx