telnyx 3.0.6 → 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 +4 -4
- data/.github/workflows/publish-gem.yml +39 -0
- data/CHANGELOG.md +5 -0
- data/VERSION +1 -1
- data/bin/publish-gem +19 -0
- data/lib/telnyx/access_token.rb +127 -0
- data/lib/telnyx/telephony_credential.rb +31 -0
- data/lib/telnyx/version.rb +1 -1
- data/lib/telnyx.rb +1 -0
- data/telnyx.gemspec +1 -0
- data/test/telnyx/access_token_test.rb +88 -0
- data/test/telnyx/telephony_credential_test.rb +59 -0
- metadata +25 -6
- data/.github/workflows/publish-gem-on-release.yml +0 -53
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84d4e73bfaa1a623aa43940a4f86155b938703fc3d7cf70ffa76b97569c70c35
|
|
4
|
+
data.tar.gz: e29d1cc3c049e1d38f2b1c49e09f365f55abac868a5684805db0fe6d72294b36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
@@ -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)
|
data/lib/telnyx/version.rb
CHANGED
data/lib/telnyx.rb
CHANGED
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
|
|
@@ -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")
|
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.
|
|
4
|
+
version: 3.1.0.pre.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Telnyx
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
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
|
|
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
|
|
@@ -391,17 +409,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
391
409
|
version: 3.0.0
|
|
392
410
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
393
411
|
requirements:
|
|
394
|
-
- - "
|
|
412
|
+
- - ">"
|
|
395
413
|
- !ruby/object:Gem::Version
|
|
396
|
-
version:
|
|
414
|
+
version: 1.3.1
|
|
397
415
|
requirements: []
|
|
398
|
-
rubygems_version: 3.
|
|
416
|
+
rubygems_version: 3.4.19
|
|
399
417
|
signing_key:
|
|
400
418
|
specification_version: 4
|
|
401
419
|
summary: Ruby bindings for the Telnyx API
|
|
402
420
|
test_files:
|
|
403
421
|
- test/api_stub_helpers.rb
|
|
404
422
|
- test/openapi/README.md
|
|
423
|
+
- test/telnyx/access_token_test.rb
|
|
405
424
|
- test/telnyx/address_test.rb
|
|
406
425
|
- test/telnyx/alphanumeric_sender_id_test.rb
|
|
407
426
|
- test/telnyx/api_operations_test.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}}"
|