telnyx 3.0.5 → 3.1.0.pre.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: 1c54ee2fae1e5040ea3a33bab7b8d103e0727f9a4bfd966d3ce33bd8c5fef194
4
- data.tar.gz: f9c16f95c3bf511e70002362b18caf01f4455bbfcb79252946b665f968b1131d
3
+ metadata.gz: 84d4e73bfaa1a623aa43940a4f86155b938703fc3d7cf70ffa76b97569c70c35
4
+ data.tar.gz: e29d1cc3c049e1d38f2b1c49e09f365f55abac868a5684805db0fe6d72294b36
5
5
  SHA512:
6
- metadata.gz: 19b78fe8dab94fc40f4ed470339e612dccd1db21b059d5ee58620f57f3d02035d1aece4778d9c0179cb9f212908c62e8bddbe26894dae231dd823293b22ba2a3
7
- data.tar.gz: 8065a019140e93780bd21c69b3479b02ebd2b90ddf1b29945e2053c768e811e12d11aa9e94f28de3ffd5f42d3a4bec576b6f8aa5b39bb132c5860cacffc901d1
6
+ metadata.gz: c40236177980cbbc70d98075f11dfceb67661a8bc4a8c8660c03b961aadc2375c79f29f637952f53e1452d549c42e3d722bde7e4a7acfd7932e5a57f7ab0800b
7
+ data.tar.gz: 8e0c010b5699e9fb409d32d2eeee5c759c36391e4360f83e834c1f375b86c1ec79fee48cf6a0ea9734f758f05103f1339ea5cb185de4a647e9eece2dd26d4dda
@@ -0,0 +1,39 @@
1
+ # Publishes the gem to rubygems.org when a GitHub release is published on the
2
+ # legacy-3.x line (pre-releases included). It can also be run manually to
3
+ # re-publish in case it failed:
4
+ # https://www.github.com/team-telnyx/telnyx-ruby/actions/workflows/publish-gem.yml
5
+ #
6
+ # Same shape as publish-gem.yml on master: RubyGems trusted publishing (OIDC),
7
+ # no API-key secret. The release tag must equal "v#{Telnyx::VERSION}".
8
+ name: Publish Gem
9
+ on:
10
+ workflow_dispatch:
11
+
12
+ release:
13
+ types: [published]
14
+
15
+ jobs:
16
+ publish:
17
+ name: publish
18
+ # Match master: the pinned setup-ruby does not support ubuntu-latest's 26.04 image.
19
+ runs-on: ubuntu-24.04
20
+ permissions:
21
+ id-token: write
22
+ contents: read
23
+
24
+ steps:
25
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
28
+ with:
29
+ ruby-version: "3.2"
30
+ bundler-cache: false
31
+
32
+ - name: Configure RubyGems Credentials
33
+ uses: rubygems/configure-rubygems-credentials@6861877652452567c1a007a50badad3d316fc9ba
34
+
35
+ - name: Publish to RubyGems.org
36
+ env:
37
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
38
+ run: |
39
+ bash ./bin/publish-gem
data/CHANGELOG.md CHANGED
@@ -1,2 +1,7 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.1.0.pre.1 - 2026-10-29
4
+ Prerelease of 3.1.0. Not resolved by `~> 3.0`; opt in with `gem "telnyx", "3.1.0.pre.1"`.
5
+ * Add `Telnyx::AccessToken` and `TelephonyCredential#to_jwt`: mint a WebRTC login token locally from the credential's `token_material`, with no request to Telnyx per token.
6
+ * Add `TelephonyCredential#fetch_token_material`, backed by `GET /v2/telephony_credentials/:id/token_material` when the object did not come from `create`.
7
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.5
1
+ 3.1.0.pre.1
data/bin/publish-gem ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Build telnyx.gemspec and push the result to rubygems.org.
3
+ # When run from a GitHub release, refuse to publish unless the release tag is
4
+ # exactly "v<Telnyx::VERSION>", so a mistagged release cannot ship the wrong version.
5
+
6
+ set -Eeuo pipefail
7
+
8
+ cd -- "$(dirname -- "$0")/.."
9
+
10
+ version=$(ruby -Ilib -e 'require "telnyx/version"; print Telnyx::VERSION')
11
+
12
+ if [ -n "${RELEASE_TAG:-}" ] && [ "$RELEASE_TAG" != "v$version" ]; then
13
+ echo "release tag '$RELEASE_TAG' does not match lib/telnyx/version.rb ('v$version')" >&2
14
+ exit 1
15
+ fi
16
+
17
+ find . -maxdepth 1 -type f -name "*.gem" -delete
18
+ gem build telnyx.gemspec
19
+ gem push "telnyx-$version.gem"
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "openssl"
6
+
7
+ module Telnyx
8
+ # Mints a WebRTC login token for a telephony credential locally, with no
9
+ # request to Telnyx.
10
+ #
11
+ # The token has the same shape as the one `POST /v2/telephony_credentials/:id/token`
12
+ # returns, so it is used the same way: pass it to the WebRTC SDK as
13
+ # `login_token`. Telnyx verifies it on login.
14
+ #
15
+ # The signing material comes from the credential: `token_material` is in the
16
+ # response of `POST /v2/telephony_credentials` and of
17
+ # `GET /v2/telephony_credentials/:id/token_material`. It is as sensitive as
18
+ # the SIP password, so keep it on the server; never ship it to a browser.
19
+ #
20
+ # credential = Telnyx::TelephonyCredential.create(connection_id: "123")
21
+ # credential.to_jwt # 1 hour
22
+ # credential.to_jwt(ttl: 600) # 10 minutes
23
+ #
24
+ # Or from stored values, without a TelephonyCredential object:
25
+ #
26
+ # Telnyx::AccessToken.new(
27
+ # credential_id: "...", user_id: "...", identity: "gencred...",
28
+ # expires_at: "2027-01-01T00:00:00Z",
29
+ # token_material: { kid: "v1", tel_token: "...", signing_secret: "..." }
30
+ # ).to_jwt
31
+ class AccessToken
32
+ DEFAULT_TTL = 3600
33
+ MAX_TTL = 86_400
34
+ CONTENT_TYPE = "telnyx-webrtc;v=1"
35
+ ALGORITHM = "HS256"
36
+
37
+ MATERIAL_FIELDS = %i[kid tel_token signing_secret].freeze
38
+
39
+ attr_reader :credential_id, :user_id, :identity, :expires_at, :ttl
40
+
41
+ # @param credential_id [String] the telephony credential `id`
42
+ # @param user_id [String] the credential's `user_id`
43
+ # @param identity [String] the credential's `sip_username`
44
+ # @param expires_at [String, nil] the credential's `expires_at`, as the API returns it
45
+ # @param token_material [Hash, TelnyxObject] `kid`, `tel_token`, `signing_secret`
46
+ # @param ttl [Integer] token lifetime in seconds, 1..86400; default one hour
47
+ def initialize(credential_id:, user_id:, identity:, token_material:, expires_at: nil, ttl: DEFAULT_TTL)
48
+ @credential_id = required(credential_id, "credential_id")
49
+ @user_id = required(user_id, "user_id")
50
+ @identity = required(identity, "identity")
51
+ @expires_at = expires_at
52
+ @material = normalize_material(token_material)
53
+ @ttl = ttl.to_i.clamp(1, MAX_TTL)
54
+ end
55
+
56
+ # The signed token, valid from now for `ttl` seconds.
57
+ def to_jwt(now: Time.now)
58
+ iat = now.to_i
59
+ header = {
60
+ alg: ALGORITHM,
61
+ typ: "JWT",
62
+ cty: CONTENT_TYPE,
63
+ kid: @material[:kid],
64
+ }
65
+ payload = {
66
+ sub: credential_id,
67
+ user_id: user_id,
68
+ identity: identity,
69
+ cred_exp: expires_at,
70
+ tel_token: @material[:tel_token],
71
+ iat: iat,
72
+ nbf: iat,
73
+ exp: iat + ttl,
74
+ jti: "#{credential_id}-#{iat}",
75
+ }
76
+
77
+ signing_input = "#{encode(JSON.generate(header))}.#{encode(JSON.generate(payload))}"
78
+ "#{signing_input}.#{encode(self.class.sign(signing_input, @material[:signing_secret]))}"
79
+ end
80
+
81
+ alias to_s to_jwt
82
+
83
+ # Decodes a token minted by this class and checks its signature against
84
+ # `signing_secret`. Returns the claims as a Hash with string keys. Meant for
85
+ # tests and debugging: Telnyx does the real verification on login.
86
+ def self.decode(jwt, signing_secret:)
87
+ head, body, sig = jwt.to_s.split(".")
88
+ raise ArgumentError, "not a JWT" if sig.nil?
89
+
90
+ expected = sign("#{head}.#{body}", signing_secret)
91
+ given = Base64.urlsafe_decode64(sig)
92
+ valid = given.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(given, expected)
93
+ raise SignatureVerificationError.new("Signature is invalid and does not match the payload", sig) unless valid
94
+
95
+ JSON.parse(Base64.urlsafe_decode64(body))
96
+ end
97
+
98
+ def self.sign(signing_input, signing_secret)
99
+ key = Base64.urlsafe_decode64(signing_secret)
100
+ OpenSSL::HMAC.digest("SHA256", key, signing_input)
101
+ end
102
+
103
+ private
104
+
105
+ def encode(bytes)
106
+ Base64.urlsafe_encode64(bytes, padding: false)
107
+ end
108
+
109
+ def required(value, name)
110
+ raise ArgumentError, "#{name} is required to mint a token" if value.nil? || value.to_s.empty?
111
+
112
+ value.to_s
113
+ end
114
+
115
+ def normalize_material(material)
116
+ hash = material.respond_to?(:to_hash) ? material.to_hash : material
117
+ raise ArgumentError, "token_material is required to mint a token" unless hash.is_a?(Hash)
118
+
119
+ MATERIAL_FIELDS.each_with_object({}) do |field, acc|
120
+ value = hash[field] || hash[field.to_s]
121
+ raise ArgumentError, "token_material.#{field} is missing" if value.nil? || value.to_s.empty?
122
+
123
+ acc[field] = value.to_s
124
+ end
125
+ end
126
+ end
127
+ end
@@ -13,10 +13,13 @@ module Telnyx
13
13
 
14
14
  def wrap(method_name, wrapper)
15
15
  define_singleton_method(method_name) do |filters = {}, opts = {}|
16
- return super(filters, opts) if filters.keys == [wrapper]
17
-
18
- filters = { wrapper => filters }
19
- super filters, opts
16
+ # If the only key is the wrapper key (e.g., 'filter'), pass it through as is
17
+ if filters.keys == [wrapper.to_sym] || filters.keys == [wrapper.to_s]
18
+ super(filters, opts)
19
+ else
20
+ # Otherwise, wrap the parameters in the wrapper key
21
+ super({ wrapper => filters }, opts)
22
+ end
20
23
  end
21
24
  end
22
25
  end
@@ -23,6 +23,37 @@ module Telnyx
23
23
  Util.convert_to_telnyx_object(resp.data, opts)
24
24
  end
25
25
 
26
+ # A WebRTC login token for this credential, minted locally with no
27
+ # request to Telnyx. Same use as `create_token`: hand it to the WebRTC
28
+ # SDK as `login_token`.
29
+ #
30
+ # The signing material (`kid`, `tel_token`, `signing_secret`) is read from
31
+ # `token_material`, present on objects that came from `create`; an object
32
+ # from `retrieve` or `list` fetches it once (see `fetch_token_material`).
33
+ # Pass `token_material:` to mint from values stored earlier, with no
34
+ # request at all.
35
+ #
36
+ # `ttl` is the token lifetime in seconds, 1..86400, default one hour.
37
+ def to_jwt(ttl: AccessToken::DEFAULT_TTL, token_material: nil, opts: {})
38
+ AccessToken.new(
39
+ credential_id: id,
40
+ user_id: self[:user_id],
41
+ identity: self[:sip_username],
42
+ expires_at: self[:expires_at],
43
+ token_material: token_material || self[:token_material] || fetch_token_material(opts),
44
+ ttl: ttl
45
+ ).to_jwt
46
+ end
47
+
48
+ # `GET /v2/telephony_credentials/:id/token_material`, fetched once and
49
+ # kept on this object. Treat the result like the SIP password.
50
+ def fetch_token_material(opts = {})
51
+ @fetch_token_material ||= begin
52
+ resp, opts = request(:get, "#{resource_url}/token_material", {}, opts)
53
+ Util.convert_to_telnyx_object(resp.data[:data] || resp.data, opts)
54
+ end
55
+ end
56
+
26
57
  # Additional action to list tags
27
58
  def self.tags(params = {}, opts = {})
28
59
  opts = Util.normalize_opts(opts)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Telnyx
4
- VERSION = "3.0.5".freeze
4
+ VERSION = "3.1.0.pre.1".freeze
5
5
  end
data/lib/telnyx.rb CHANGED
@@ -38,6 +38,7 @@ require "telnyx/list_object"
38
38
  require "telnyx/api_resource"
39
39
  require "telnyx/singleton_api_resource"
40
40
  require "telnyx/webhook"
41
+ require "telnyx/access_token"
41
42
 
42
43
  require "telnyx/address"
43
44
  require "telnyx/alphanumeric_sender_id"
data/telnyx.gemspec CHANGED
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
25
25
  s.add_dependency("faraday", ">= 2.5.0")
26
26
  s.add_dependency("faraday-multipart", "~> 1.0.4")
27
27
  s.add_dependency("faraday-net_http_persistent", "~> 2.1.0")
28
+ s.add_dependency("faraday-retry", "~> 2.0")
28
29
  s.add_dependency("net-http-persistent", ">= 3.0", "< 5.0")
29
30
  s.add_dependency("ed25519", "~> 1")
30
31
 
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ module Telnyx
6
+ class AccessTokenTest < Test::Unit::TestCase
7
+ SECRET = Base64.urlsafe_encode64(OpenSSL::Random.random_bytes(32), padding: false)
8
+ MATERIAL = {
9
+ kid: "v1",
10
+ tel_token: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
11
+ signing_secret: SECRET,
12
+ }.freeze
13
+
14
+ def token(**overrides)
15
+ AccessToken.new(
16
+ **{
17
+ credential_id: "c90a3e6d-8c8c-4c5a-a8b1-6b8d0f2f8a31",
18
+ user_id: "191d61ea-1cb5-445a-8dd1-384f45e7cb7a",
19
+ identity: "gencred1234",
20
+ expires_at: "2027-01-01T00:00:00Z",
21
+ token_material: MATERIAL,
22
+ }.merge(overrides)
23
+ )
24
+ end
25
+
26
+ should "mint a three part HS256 JWT with the Telnyx content type" do
27
+ jwt = token.to_jwt
28
+ head, body, sig = jwt.split(".")
29
+ assert_equal 3, jwt.split(".").size
30
+ assert_nil jwt[%r{[+/=]}], "token must be base64url without padding"
31
+
32
+ header = JSON.parse(Base64.urlsafe_decode64(head))
33
+ assert_equal({ "alg" => "HS256", "typ" => "JWT", "cty" => "telnyx-webrtc;v=1", "kid" => "v1" }, header)
34
+
35
+ expected = OpenSSL::HMAC.digest("SHA256", Base64.urlsafe_decode64(SECRET), "#{head}.#{body}")
36
+ assert_equal Base64.urlsafe_encode64(expected, padding: false), sig
37
+ end
38
+
39
+ should "carry the credential and the tel_token in the claims" do
40
+ now = Time.at(1_760_000_000)
41
+ claims = AccessToken.decode(token.to_jwt(now: now), signing_secret: SECRET)
42
+
43
+ assert_equal "c90a3e6d-8c8c-4c5a-a8b1-6b8d0f2f8a31", claims["sub"]
44
+ assert_equal "191d61ea-1cb5-445a-8dd1-384f45e7cb7a", claims["user_id"]
45
+ assert_equal "gencred1234", claims["identity"]
46
+ assert_equal "2027-01-01T00:00:00Z", claims["cred_exp"]
47
+ assert_equal MATERIAL[:tel_token], claims["tel_token"]
48
+ assert_equal now.to_i, claims["iat"]
49
+ assert_equal now.to_i, claims["nbf"]
50
+ assert_equal now.to_i + 3600, claims["exp"]
51
+ assert_equal "c90a3e6d-8c8c-4c5a-a8b1-6b8d0f2f8a31-#{now.to_i}", claims["jti"]
52
+ assert_kind_of Integer, claims["exp"]
53
+ end
54
+
55
+ should "default to one hour and cap the ttl at 24 hours" do
56
+ now = Time.at(1_760_000_000)
57
+ assert_equal now.to_i + 3600, AccessToken.decode(token.to_jwt(now: now), signing_secret: SECRET)["exp"]
58
+ assert_equal now.to_i + 600, AccessToken.decode(token(ttl: 600).to_jwt(now: now), signing_secret: SECRET)["exp"]
59
+ assert_equal now.to_i + 86_400, AccessToken.decode(token(ttl: 10**6).to_jwt(now: now), signing_secret: SECRET)["exp"]
60
+ assert_equal now.to_i + 1, AccessToken.decode(token(ttl: 0).to_jwt(now: now), signing_secret: SECRET)["exp"]
61
+ end
62
+
63
+ should "accept token_material with string keys or as a TelnyxObject" do
64
+ strings = MATERIAL.transform_keys(&:to_s)
65
+ object = TelnyxObject.construct_from(MATERIAL)
66
+ a = token(token_material: strings).to_jwt(now: Time.at(0))
67
+ b = token(token_material: object).to_jwt(now: Time.at(0))
68
+ assert_equal a, b
69
+ end
70
+
71
+ should "refuse to mint without the material or the identity" do
72
+ assert_raise(ArgumentError) { token(token_material: nil) }
73
+ assert_raise(ArgumentError) { token(token_material: MATERIAL.reject { |k, _| k == :signing_secret }) }
74
+ assert_raise(ArgumentError) { token(token_material: MATERIAL.merge(tel_token: "")) }
75
+ assert_raise(ArgumentError) { token(identity: nil) }
76
+ assert_raise(ArgumentError) { token(user_id: "") }
77
+ end
78
+
79
+ should "decode rejects a token signed with another secret" do
80
+ other = Base64.urlsafe_encode64(OpenSSL::Random.random_bytes(32), padding: false)
81
+ jwt = token.to_jwt
82
+ assert_raise(SignatureVerificationError) { AccessToken.decode(jwt, signing_secret: other) }
83
+
84
+ tampered = jwt.sub(/\.[^.]+\./, ".#{Base64.urlsafe_encode64('{"sub":"x"}', padding: false)}.")
85
+ assert_raise(SignatureVerificationError) { AccessToken.decode(tampered, signing_secret: SECRET) }
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ module Telnyx
6
+ class ParamWrapperTest < Test::Unit::TestCase
7
+ class TestResource < APIResource
8
+ extend Telnyx::APIOperations::List
9
+ extend Telnyx::APIOperations::ParamWrapper
10
+ wrap "list", "filter"
11
+
12
+ OBJECT_NAME = "test_resource"
13
+
14
+ # Override request to avoid actual API calls
15
+ def self.request(method, url, params = {}, opts = {})
16
+ # Store the parameters for verification
17
+ @@last_request_params = params
18
+
19
+ # Return a mock response
20
+ resp = Struct.new(:data).new({ data: [] })
21
+ [resp, {}]
22
+ end
23
+
24
+ # Method to retrieve the last request parameters
25
+ def self.last_request_params
26
+ @@last_request_params
27
+ end
28
+ end
29
+
30
+ context ".list" do
31
+ should "correctly handle parameters when filter is explicitly provided" do
32
+ params = {
33
+ filter: {
34
+ country_code: "US",
35
+ limit: 1,
36
+ phone_number: { starts_with: "123" }
37
+ }
38
+ }
39
+
40
+ TestResource.list(params)
41
+
42
+ # Verify the parameters were passed correctly - note that keys may be strings or symbols
43
+ params = TestResource.last_request_params
44
+
45
+ # Convert all keys to symbols for consistent comparison
46
+ params_with_symbol_keys = {}
47
+ params.each do |k, v|
48
+ params_with_symbol_keys[k.to_sym] = v
49
+ end
50
+
51
+ assert_equal(
52
+ { filter: { country_code: "US", limit: 1, phone_number: { starts_with: "123" } } },
53
+ params_with_symbol_keys
54
+ )
55
+ end
56
+
57
+ should "correctly wrap parameters when filter is not explicitly provided" do
58
+ params = {
59
+ country_code: "US",
60
+ limit: 1,
61
+ phone_number: { starts_with: "123" }
62
+ }
63
+
64
+ TestResource.list(params)
65
+
66
+ # Verify the parameters were wrapped correctly - note that keys may be strings or symbols
67
+ params = TestResource.last_request_params
68
+
69
+ # Convert all keys to symbols for consistent comparison
70
+ params_with_symbol_keys = {}
71
+ params.each do |k, v|
72
+ params_with_symbol_keys[k.to_sym] = v
73
+ end
74
+
75
+ assert_equal(
76
+ { filter: { country_code: "US", limit: 1, phone_number: { starts_with: "123" } } },
77
+ params_with_symbol_keys
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
@@ -61,6 +61,65 @@ module Telnyx
61
61
  assert_requested :get, "#{Telnyx.api_base}/v2/telephony_credentials/tags"
62
62
  end
63
63
  end
64
+
65
+ CREDENTIAL = {
66
+ record_type: "credential",
67
+ id: "c90a3e6d-8c8c-4c5a-a8b1-6b8d0f2f8a31",
68
+ user_id: "191d61ea-1cb5-445a-8dd1-384f45e7cb7a",
69
+ sip_username: "gencred1234",
70
+ sip_password: "secret",
71
+ expires_at: "2027-01-01T00:00:00Z",
72
+ }.freeze
73
+ MATERIAL = {
74
+ kid: "v1",
75
+ tel_token: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
76
+ signing_secret: Base64.urlsafe_encode64("k" * 32, padding: false),
77
+ }.freeze
78
+ URL = "#{PROXY_BASE_URL}/v2/telephony_credentials/#{CREDENTIAL[:id]}".freeze
79
+
80
+ context "local token minting" do
81
+ should "mint from the create response without another request" do
82
+ stub_request(:post, "#{PROXY_BASE_URL}/v2/telephony_credentials")
83
+ .to_return(body: JSON.generate(data: CREDENTIAL.merge(token_material: MATERIAL)))
84
+
85
+ credential = TelephonyCredential.create(connection_id: "1234567890")
86
+ jwt = credential.to_jwt(ttl: 600)
87
+
88
+ claims = AccessToken.decode(jwt, signing_secret: MATERIAL[:signing_secret])
89
+ assert_equal CREDENTIAL[:id], claims["sub"]
90
+ assert_equal CREDENTIAL[:sip_username], claims["identity"]
91
+ assert_equal MATERIAL[:tel_token], claims["tel_token"]
92
+ assert_equal claims["iat"] + 600, claims["exp"]
93
+ assert_not_requested :get, "#{URL}/token_material"
94
+ end
95
+
96
+ should "fetch token_material once for a retrieved credential" do
97
+ stub_request(:get, URL).to_return(body: JSON.generate(data: CREDENTIAL))
98
+ stub_request(:get, "#{URL}/token_material").to_return(body: JSON.generate(data: MATERIAL))
99
+
100
+ credential = TelephonyCredential.retrieve(CREDENTIAL[:id])
101
+ first = credential.to_jwt(ttl: 60)
102
+ second = credential.to_jwt(ttl: 60)
103
+
104
+ assert_requested :get, "#{URL}/token_material", times: 1
105
+ assert_equal MATERIAL[:kid], credential.fetch_token_material[:kid]
106
+ %w[sub user_id identity tel_token].each do |claim|
107
+ assert_equal AccessToken.decode(first, signing_secret: MATERIAL[:signing_secret])[claim],
108
+ AccessToken.decode(second, signing_secret: MATERIAL[:signing_secret])[claim]
109
+ end
110
+ end
111
+
112
+ should "mint from material the caller stored, with no request at all" do
113
+ stub_request(:get, URL).to_return(body: JSON.generate(data: CREDENTIAL))
114
+ credential = TelephonyCredential.retrieve(CREDENTIAL[:id])
115
+
116
+ jwt = credential.to_jwt(token_material: MATERIAL.transform_keys(&:to_s))
117
+
118
+ assert_not_requested :get, "#{URL}/token_material"
119
+ assert_equal CREDENTIAL[:user_id], AccessToken.decode(jwt, signing_secret: MATERIAL[:signing_secret])["user_id"]
120
+ end
121
+ end
122
+
64
123
  # Issue with response(RESPONSE SCHEMA: text/plain)
65
124
  # should "create telephony credential token" do
66
125
  # telephony_credential = TelephonyCredential.retrieve("id")
@@ -0,0 +1,51 @@
1
+ require 'bundler/setup'
2
+ require 'telnyx'
3
+
4
+ # Set a dummy API key
5
+ Telnyx.api_key = 'TEST_API_KEY'
6
+
7
+ # Monkey patch the request method to capture parameters without making API calls
8
+ module Telnyx
9
+ module APIOperations
10
+ module Request
11
+ module ClassMethods
12
+ alias original_request request
13
+
14
+ def request(method, url, params = {}, opts = {})
15
+ puts "Method: #{method}"
16
+ puts "URL: #{url}"
17
+ puts "Raw params: #{params.inspect}"
18
+
19
+ if method.to_s.downcase.to_sym == :get
20
+ puts "URL-encoded params: #{Telnyx::Util.encode_parameters(params)}"
21
+ else
22
+ puts "JSON body: #{JSON.generate(params)}"
23
+ end
24
+
25
+ # Return a mock response
26
+ mock_response = Struct.new(:data).new({data: []})
27
+ [mock_response, {}]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # Test with the parameters from the example
35
+ puts "\n=== Testing with filter hash ==="
36
+ response = Telnyx::AvailablePhoneNumber.list(
37
+ filter: {
38
+ country_code: "US",
39
+ limit: 1,
40
+ national_destination_code: "206",
41
+ phone_number: { starts_with: "4532888" }
42
+ }
43
+ )
44
+
45
+ puts "\n=== Testing with direct parameters ==="
46
+ response = Telnyx::AvailablePhoneNumber.list(
47
+ country_code: "US",
48
+ limit: 1,
49
+ national_destination_code: "206",
50
+ phone_number: { starts_with: "4532888" }
51
+ )
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telnyx
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.5
4
+ version: 3.1.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Telnyx
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-18 00:00:00.000000000 Z
11
+ date: 2026-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 2.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday-retry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: net-http-persistent
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -90,13 +104,14 @@ description: Telnyx enables anyone to deliver enterprise-grade real-time communi
90
104
  over the internet. See https://telnyx.com for details.
91
105
  email: support@telnyx.com
92
106
  executables:
107
+ - publish-gem
93
108
  - telnyx-console
94
109
  extensions: []
95
110
  extra_rdoc_files: []
96
111
  files:
97
112
  - ".github/ISSUE_TEMPLATE.md"
98
113
  - ".github/scripts/before_install.sh"
99
- - ".github/workflows/publish-gem-on-release.yml"
114
+ - ".github/workflows/publish-gem.yml"
100
115
  - ".github/workflows/ruby.yml"
101
116
  - ".github/workflows/tweet-release.yml"
102
117
  - ".gitignore"
@@ -112,6 +127,7 @@ files:
112
127
  - Rakefile
113
128
  - Spec3.json
114
129
  - VERSION
130
+ - bin/publish-gem
115
131
  - bin/telnyx-console
116
132
  - examples/fax/Gemfile
117
133
  - examples/fax/config.yaml
@@ -121,6 +137,7 @@ files:
121
137
  - lib/telnyx.rb
122
138
  - lib/telnyx/access_control_ip.rb
123
139
  - lib/telnyx/access_ip_range.rb
140
+ - lib/telnyx/access_token.rb
124
141
  - lib/telnyx/address.rb
125
142
  - lib/telnyx/alphanumeric_sender_id.rb
126
143
  - lib/telnyx/api_operations/create.rb
@@ -261,6 +278,7 @@ files:
261
278
  - telnyx.gemspec
262
279
  - test/api_stub_helpers.rb
263
280
  - test/openapi/README.md
281
+ - test/telnyx/access_token_test.rb
264
282
  - test/telnyx/address_test.rb
265
283
  - test/telnyx/alphanumeric_sender_id_test.rb
266
284
  - test/telnyx/api_operations_test.rb
@@ -326,6 +344,7 @@ files:
326
344
  - test/telnyx/number_reservation_test.rb
327
345
  - test/telnyx/ota_update_test.rb
328
346
  - test/telnyx/outbound_voice_profile_test.rb
347
+ - test/telnyx/param_wrapper_test.rb
329
348
  - test/telnyx/phone_number_block_job_test.rb
330
349
  - test/telnyx/phone_number_regulatory_requirement_test.rb
331
350
  - test/telnyx/phone_number_test.rb
@@ -370,6 +389,7 @@ files:
370
389
  - test/telnyx_test.rb
371
390
  - test/test_data.rb
372
391
  - test/test_helper.rb
392
+ - test/test_params.rb
373
393
  homepage: https://developers.telnyx.com
374
394
  licenses:
375
395
  - MIT
@@ -378,7 +398,7 @@ metadata:
378
398
  github_repo: ssh://github.com/team-telnyx/telnyx-ruby
379
399
  homepage_uri: https://telnyx.com
380
400
  source_code_uri: https://github.com/team-telnyx/telnyx-ruby
381
- post_install_message:
401
+ post_install_message:
382
402
  rdoc_options: []
383
403
  require_paths:
384
404
  - lib
@@ -389,17 +409,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
389
409
  version: 3.0.0
390
410
  required_rubygems_version: !ruby/object:Gem::Requirement
391
411
  requirements:
392
- - - ">="
412
+ - - ">"
393
413
  - !ruby/object:Gem::Version
394
- version: '0'
414
+ version: 1.3.1
395
415
  requirements: []
396
- rubygems_version: 3.2.15
397
- signing_key:
416
+ rubygems_version: 3.4.19
417
+ signing_key:
398
418
  specification_version: 4
399
419
  summary: Ruby bindings for the Telnyx API
400
420
  test_files:
401
421
  - test/api_stub_helpers.rb
402
422
  - test/openapi/README.md
423
+ - test/telnyx/access_token_test.rb
403
424
  - test/telnyx/address_test.rb
404
425
  - test/telnyx/alphanumeric_sender_id_test.rb
405
426
  - test/telnyx/api_operations_test.rb
@@ -465,6 +486,7 @@ test_files:
465
486
  - test/telnyx/number_reservation_test.rb
466
487
  - test/telnyx/ota_update_test.rb
467
488
  - test/telnyx/outbound_voice_profile_test.rb
489
+ - test/telnyx/param_wrapper_test.rb
468
490
  - test/telnyx/phone_number_block_job_test.rb
469
491
  - test/telnyx/phone_number_regulatory_requirement_test.rb
470
492
  - test/telnyx/phone_number_test.rb
@@ -509,3 +531,4 @@ test_files:
509
531
  - test/telnyx_test.rb
510
532
  - test/test_data.rb
511
533
  - test/test_helper.rb
534
+ - test/test_params.rb
@@ -1,53 +0,0 @@
1
- on:
2
- release:
3
- types: [released]
4
-
5
- jobs:
6
- #Deprecating, incoming replacement for self-hosted mock service
7
- #test:
8
- #env:
9
- # TELNYX_MOCK_OPEN_API_URI: https://raw.githubusercontent.com/team-telnyx/openapi/master/openapi/spec3.json
10
- #runs-on: ubuntu-latest
11
- #strategy:
12
- # matrix:
13
- # ruby: [ '3.0', '3.1', '3.2' ]
14
- #steps:
15
- #- uses: actions/checkout@v4
16
- #- name: Set up Ruby
17
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
18
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
19
- # uses: ruby/setup-ruby@v1
20
- # uses: ruby/setup-ruby@v1
21
- # with:
22
- # ruby-version: ${{ matrix.ruby }}
23
- #- name: Run Setup Script to install prism mock
24
- # run: source ./.github/scripts/before_install.sh
25
- # shell: bash
26
- #- name: Install dependencies
27
- # run: bundle install
28
- #- name: Run tests
29
- # run: bundle exec rake
30
-
31
- build_and_publish_gem:
32
- name: Build + Publish Gem
33
- runs-on: ubuntu-latest
34
- permissions:
35
- packages: write
36
- contents: read
37
- steps:
38
- - uses: actions/checkout@v2
39
- - name: Set up Ruby 3.0
40
- uses: ruby/setup-ruby@477b21f02be01bcb8030d50f37cfec92bfa615b6
41
- with:
42
- ruby-version: 3.0
43
- - run: bundle install
44
- - name: Publish to RubyGems
45
- run: |
46
- mkdir -p $HOME/.gem
47
- touch $HOME/.gem/credentials
48
- chmod 0600 $HOME/.gem/credentials
49
- printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
50
- gem build *.gemspec
51
- gem push *.gem
52
- env:
53
- GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_API_KEY}}"