anycable-core 1.4.4 → 1.5.0.rc.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: b02bcc10302bdd445b92e77c0ce354734e82a0ba124c9e88b2997b87013bbf3d
4
- data.tar.gz: dadf0fdc856292e614f39ebd43b996da49ce4e9771408429ba75fae7ace9877b
3
+ metadata.gz: b3469966952c29b422b3177acbb3402fc9bf500786d55774f9b3311b904548bb
4
+ data.tar.gz: 7e5a7302b61c07af90f61b11878212a464ba1eac0f7d6b664f91b8dcbd3fe9e6
5
5
  SHA512:
6
- metadata.gz: 918addcfda8aea6f57eb7fd4872a4b93eeafc18862e93d75221d55f13ded38c218c231a47fbf1b803ca717618a518a05b0c6bf86b080000aa566281842033167
7
- data.tar.gz: f96deb1656000ce9d2089f94da40860bb9820598a5afb002a7ae5c2bc28f71f799dad6efeeea824e7bf306ceb8b76aad109b824c675391d4f218a81f90d9e491
6
+ metadata.gz: 57e7aafe42eeddf359db963a688cf89acf9b109935f4561ba2eb4cb9c994e3a47d44b721b24f717be41630d4b8234d5f77a12d91246b0033e2385772287c43af
7
+ data.tar.gz: 53785ae7eaf8c86d1f164a89dbfeaee2063be1c4180f2a263cb2f47ca3ca41bc528f545ba65c1232c691fa6515bd4be39e9f6200b903b9b2bb8af02f09f37ccc
data/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## master
4
4
 
5
+ - Added JWT utils. ([@palkan][])
6
+
7
+ You can now generate and verify AnyCable JWT tokens without using additional dependencies:
8
+
9
+ ```ruby
10
+ token = AnyCable::JWT.encode({user_id: "1"})
11
+
12
+ identifiers = AnyCable::JWT.decode(token)
13
+ ```
14
+
15
+ This change will deprecate `anycable-rails-jwt`.
16
+
17
+ - Added signed streams support. ([@palkan][])
18
+
19
+ You can generate signed stream names as follows:
20
+
21
+ ```ruby
22
+ signed_name = AnyCable::Streams.sign("chat/2024")
23
+ ```
24
+
25
+ - Added `secret` and `broadcast_key` configuration parameters. ([@palkan][])
26
+
5
27
  ## 1.4.3 (2023-10-15)
6
28
 
7
29
  - Add broadcast options support. ([@palkan][])
@@ -40,15 +40,21 @@ module AnyCable
40
40
  MAX_ATTEMPTS = 3
41
41
  DELAY = 2
42
42
 
43
- attr_reader :url, :headers, :authorized
44
- alias_method :authorized?, :authorized
43
+ attr_reader :url, :headers, :authorization
45
44
 
46
- def initialize(url: AnyCable.config.http_broadcast_url, secret: AnyCable.config.http_broadcast_secret)
45
+ def initialize(url: AnyCable.config.http_broadcast_url, secret: AnyCable.config.broadcast_key)
47
46
  @url = url
48
47
  @headers = {}
48
+ @authorization = nil
49
+
50
+ if !secret
51
+ secret = AnyCable.config.broadcast_key!
52
+ @authorization = "with authorization key inferred from the application secret" if secret
53
+ end
54
+
49
55
  if secret
50
56
  headers["Authorization"] = "Bearer #{secret}"
51
- @authorized = true
57
+ @authorization ||= "with authorization"
52
58
  end
53
59
 
54
60
  @uri = URI.parse(url)
@@ -71,7 +77,7 @@ module AnyCable
71
77
  end
72
78
 
73
79
  def announce!
74
- logger.info "Broadcasting HTTP url: #{url}#{authorized? ? " (with authorization)" : ""}"
80
+ logger.info "Broadcasting HTTP url: #{url} (#{authorization || "no authorization"})"
75
81
  end
76
82
 
77
83
  private
@@ -7,6 +7,11 @@ require "uri"
7
7
  module AnyCable
8
8
  # AnyCable configuration.
9
9
  class Config < Anyway::Config
10
+ # These phareses are used to infer secret keys from the application secret
11
+ # and MUST match the ones used in AnyCable (Go)
12
+ BROADCAST_SECRET_PHRASE = "broadcast-cable"
13
+ HTTP_RPC_SECRET_PHRASE = "rpc-cable"
14
+
10
15
  class << self
11
16
  # Add usage txt for CLI
12
17
  def usage(txt)
@@ -22,9 +27,18 @@ module AnyCable
22
27
 
23
28
  attr_config(
24
29
  presets: "",
30
+ secret: nil,
31
+
32
+ ## Streams
33
+ streams_secret: nil,
25
34
 
26
- ## PubSub
35
+ ## JWT
36
+ jwt_secret: nil,
37
+ jwt_ttl: 3600, # 1 hour
38
+
39
+ ## Broadcasting
27
40
  broadcast_adapter: :redis,
41
+ broadcast_key: nil,
28
42
 
29
43
  ### Redis options
30
44
  redis_url: ENV.fetch("REDIS_URL", "redis://localhost:6379"),
@@ -42,6 +56,7 @@ module AnyCable
42
56
 
43
57
  ### HTTP broadcasting options
44
58
  http_broadcast_url: "http://localhost:8090/_broadcast",
59
+ # DEPRECATED: use `broadcast_key` instead
45
60
  http_broadcast_secret: nil,
46
61
 
47
62
  ### Logging options
@@ -86,36 +101,67 @@ module AnyCable
86
101
  !http_health_port.nil? && http_health_port != ""
87
102
  end
88
103
 
104
+ def broadcast_key!
105
+ if http_broadcast_secret && !broadcast_key
106
+ self.broadcast_key ||= http_broadcast_secret
107
+ warn "DEPRECATION WARNING: `http_broadcast_secret` is deprecated, use `broadcast_key` instead"
108
+ end
109
+
110
+ return broadcast_key if broadcast_key
111
+ return unless secret
112
+
113
+ self.broadcast_key = infer_from_application_secret(BROADCAST_SECRET_PHRASE)
114
+ end
115
+
116
+ def http_rpc_secret!
117
+ return http_rpc_secret if http_rpc_secret
118
+ return unless secret
119
+
120
+ self.http_rpc_secret = infer_from_application_secret(HTTP_RPC_SECRET_PHRASE)
121
+ end
122
+
123
+ def streams_secret
124
+ super || secret
125
+ end
126
+
127
+ def jwt_secret
128
+ super || secret
129
+ end
130
+
89
131
  usage <<~TXT
90
132
  APPLICATION
91
- --broadcast-adapter=type Pub/sub adapter type for broadcasts, default: redis
92
- --log-level=level Logging level, default: "info"
93
- --log-file=path Path to log file, default: <none> (log to STDOUT)
94
- --debug Turn on verbose logging ("debug" level and verbose logging on)
133
+ --broadcast-adapter=type Broadcasting adapter, default: redis
134
+ --secret=secret Application secret, default: <none>
135
+ --broadcast-key=key Broadcasting secret key, default: <none> (inferred from the application secret if any)
136
+ --streams-secret=secret Signed streams secret, default: <none> (inferred from the application secret if any)
95
137
 
96
138
  HTTP HEALTH CHECKER
97
139
  --http-health-port=port Port to run HTTP health server on, default: <none> (disabled)
98
140
  --http-health-path=path Endpoint to serve health checks, default: "/health"
99
141
 
100
- REDIS PUB/SUB
101
- --redis-url=url Redis URL for pub/sub, default: REDIS_URL or "redis://localhost:6379"
142
+ REDIS
143
+ --redis-url=url Redis URL for broadcasting, default: REDIS_URL or "redis://localhost:6379"
102
144
  --redis-channel=name Redis channel for broadcasting, default: "__anycable__"
103
145
  --redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
104
146
  --redis-tls-verify=yes|no Whether to perform server certificate check in case of rediss:// protocol. Default: yes
105
147
  --redis-tls-client_cert-path=path Default: nil
106
148
  --redis-tls-client_key-path=path Default: nil
107
149
 
108
- NATS PUB/SUB
109
- --nats-servers=<...addresses> NATS servers for pub/sub, default: "nats://localhost:4222"
150
+ NATS
151
+ --nats-servers=<...addresses> NATS servers for broadcasting, default: "nats://localhost:4222"
110
152
  --nats-channel=name NATS channel for broadcasting, default: "__anycable__"
111
153
  --nats-dont-randomize-servers Pass this option to disable NATS servers randomization during (re-)connect
112
154
 
113
- HTTP PUB/SUB
114
- --http-broadcast-url HTTP pub/sub endpoint URL, default: "http://localhost:8090/_broadcast"
115
- --http-broadcast-secret HTTP pub/sub authorization secret, default: <none> (disabled)
155
+ HTTP BROADCASTING
156
+ --http-broadcast-url HTTP broadcasting endpoint URL, default: "http://localhost:8090/_broadcast"
116
157
 
117
158
  HTTP RPC
118
- --http-rpc-secret HTTP RPC authorization secret, default: <none> (disabled)
159
+ --http-rpc-secret HTTP RPC authorization secret, default: <none> (inferred from the application secret if any)
160
+
161
+ LOGGING
162
+ --log-level=level Logging level, default: "info"
163
+ --log-file=path Path to log file, default: <none> (log to STDOUT)
164
+ --debug Turn on verbose logging ("debug" level and verbose logging on)
119
165
  TXT
120
166
 
121
167
  # Build Redis parameters
@@ -216,5 +262,14 @@ module AnyCable
216
262
  write_config_attr(key, value)
217
263
  __trace__&.record_value(value, key, type: :preset, preset: preset)
218
264
  end
265
+
266
+ def infer_from_application_secret(phrase)
267
+ app_secret = secret
268
+ return unless app_secret
269
+
270
+ require "openssl"
271
+
272
+ OpenSSL::HMAC.hexdigest("SHA256", app_secret, phrase)
273
+ end
219
274
  end
220
275
  end
@@ -4,6 +4,11 @@ module AnyCable
4
4
  module HTTRPC
5
5
  class Server
6
6
  def initialize(token: AnyCable.config.http_rpc_secret)
7
+ if !token
8
+ token = AnyCable.config.http_rpc_secret!
9
+ AnyCable.logger.info("AnyCable HTTP RPC created with authorization key inferred from the application secret")
10
+ end
11
+
7
12
  @token = token
8
13
  end
9
14
 
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+
6
+ module AnyCable
7
+ module JWT
8
+ class DecodeError < StandardError; end
9
+
10
+ class VerificationError < DecodeError; end
11
+
12
+ class ExpiredSignature < DecodeError; end
13
+
14
+ # Basic JWT encode/decode implementation suitable to our needs
15
+ # and not requiring external dependencies
16
+ module BasicImpl
17
+ ALGORITHM = "HS256"
18
+
19
+ class << self
20
+ def encode(payload, secret_key)
21
+ payload = ::Base64.urlsafe_encode64(payload.to_json, padding: false)
22
+ headers = ::Base64.urlsafe_encode64({"alg" => ALGORITHM}.to_json, padding: false)
23
+
24
+ header = "#{headers}.#{payload}"
25
+ signature = sign(header, secret_key)
26
+
27
+ "#{header}.#{signature}"
28
+ end
29
+
30
+ def decode(token, secret_key)
31
+ header, payload, signature = token.split(".")
32
+ # Check segments
33
+ raise DecodeError, "Not enough or too many segments" unless header && payload && signature
34
+
35
+ # Verify the algorithm
36
+ decoded_header = ::JSON.parse(::Base64.urlsafe_decode64(header))
37
+ raise DecodeError, "Algorithm not supported" unless decoded_header["alg"] == ALGORITHM
38
+
39
+ # Verify the signature
40
+ expected_signature = sign("#{header}.#{payload}", secret_key)
41
+ raise VerificationError, "Signature verification failed" unless secure_compare(signature, expected_signature)
42
+
43
+ # Verify expiration
44
+ decoded_payload = ::JSON.parse(::Base64.urlsafe_decode64(payload))
45
+ if decoded_payload.key?("exp")
46
+ raise ExpiredSignature, "Signature has expired" if Time.now.to_i >= decoded_payload["exp"]
47
+ end
48
+
49
+ decoded_payload
50
+ rescue JSON::ParserError, ArgumentError
51
+ raise DecodeError, "Invalid segment encoding"
52
+ end
53
+
54
+ # We don't really care about timing attacks here,
55
+ # since verification is done on the AnyCable server side.
56
+ # But still, we can use constant-time comparison when it's available.
57
+ if OpenSSL.respond_to?(:fixed_length_secure_compare)
58
+ def secure_compare(a, b)
59
+ return false if a.bytesize != b.bytesize
60
+
61
+ OpenSSL.fixed_length_secure_compare(a, b)
62
+ end
63
+ else
64
+ def secure_compare(a, b)
65
+ return false if a.bytesize != b.bytesize
66
+
67
+ a == b
68
+ end
69
+ end
70
+
71
+ def sign(data, secret_key)
72
+ ::Base64.urlsafe_encode64(
73
+ ::OpenSSL::HMAC.digest("SHA256", secret_key, data),
74
+ padding: false
75
+ )
76
+ end
77
+ end
78
+ end
79
+
80
+ class << self
81
+ attr_accessor :jwt_impl
82
+
83
+ def encode(payload, expires_at: nil, secret_key: AnyCable.config.jwt_secret, ttl: AnyCable.config.jwt_ttl)
84
+ raise ArgumentError, "JWT encryption key is not specified. Add it via `jwt_secret` or `secret` option" if secret_key.nil? || secret_key.empty?
85
+
86
+ encoded = Serializer.serialize(payload).to_json
87
+
88
+ data = {ext: encoded}
89
+
90
+ data[:exp] = expires_at.to_i if expires_at
91
+
92
+ if ttl&.positive? && !data.key?(:exp)
93
+ data[:exp] = Time.now.to_i + ttl
94
+ end
95
+
96
+ jwt_impl.encode(data, secret_key)
97
+ end
98
+
99
+ def decode(token, secret_key: AnyCable.config.jwt_secret)
100
+ raise ArgumentError, "JWT encryption key is not specified. Add it via `jwt_secret` or `secret` option" if secret_key.nil? || secret_key.empty?
101
+
102
+ jwt_impl.decode(token, secret_key).then do |decoded|
103
+ ::JSON.parse(decoded.fetch("ext"), symbolize_names: true)
104
+ end.then { |data| Serializer.deserialize(data) }
105
+ end
106
+ end
107
+
108
+ self.jwt_impl = BasicImpl
109
+ end
110
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ # Serializer is responsible for converting Ruby objects to and from a transferrable format (e.g., for identifiers, connection/channel state, etc.).
5
+ # It relies on configurable `.object_serializer` to handle non-primitive values and handles Hash/Array seririlzation.
6
+ module Serializer
7
+ class << self
8
+ attr_accessor :object_serializer
9
+
10
+ def serialize(obj)
11
+ handled = object_serializer&.serialize(obj)
12
+ return handled if handled
13
+
14
+ case obj
15
+ when nil, true, false, Integer, Float, String, Symbol
16
+ obj
17
+ when Hash
18
+ obj.transform_values { |v| serialize(v) }
19
+ when Array
20
+ obj.map { |v| serialize(v) }
21
+ else
22
+ raise ArgumentError, "Can't serialize #{obj.inspect}"
23
+ end
24
+ end
25
+
26
+ # Deserialize previously serialized value to a Ruby object.
27
+ def deserialize(val)
28
+ if val.is_a?(::String)
29
+ handled = object_serializer&.deserialize(val)
30
+ return handled if handled
31
+ end
32
+
33
+ case val
34
+ when Hash
35
+ val.transform_values { |v| deserialize(v) }
36
+ when Array
37
+ val.map { |v| deserialize(v) }
38
+ else
39
+ val
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+ require "json"
6
+
7
+ module AnyCable
8
+ module Streams
9
+ class << self
10
+ def signed(stream_name)
11
+ ::Base64.urlsafe_encode64(::JSON.dump(stream_name)).then do |encoded|
12
+ "#{encoded}--#{signature(encoded)}"
13
+ end
14
+ end
15
+
16
+ def verified(signed_stream_name)
17
+ encoded, sig = signed_stream_name.split("--")
18
+ raise ArgumentError, "stream name has incorrect format" unless encoded
19
+
20
+ return unless sig == signature(encoded)
21
+
22
+ ::Base64.urlsafe_decode64(encoded).then do |decoded|
23
+ ::JSON.parse(decoded)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def signature(val)
30
+ key = AnyCable.config.streams_secret
31
+
32
+ raise ArgumentError, "streams signing secret is missing" unless key
33
+
34
+ ::OpenSSL::HMAC.hexdigest("SHA256", key, val)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- VERSION = "1.4.4"
4
+ VERSION = "1.5.0.rc.1"
5
5
  end
data/lib/anycable.rb CHANGED
@@ -26,6 +26,10 @@ require "anycable/httrpc/server"
26
26
  #
27
27
  # Broadcasting messages to WS is done through _broadcast adapter_ (Redis Pub/Sub by default).
28
28
  module AnyCable
29
+ autoload :JWT, "anycable/jwt"
30
+ autoload :Serializer, "anycable/serializer"
31
+ autoload :Streams, "anycable/streams"
32
+
29
33
  class << self
30
34
  # Provide connection factory which
31
35
  # is a callable object with build
@@ -4,6 +4,16 @@ module AnyCable
4
4
  def presets=: (Array[String]) -> void
5
5
  def broadcast_adapter: () -> Symbol
6
6
  def broadcast_adapter=: (Symbol) -> void
7
+ def secret: () -> String?
8
+ def secret=: (String?) -> void
9
+ def streams_secret: () -> String?
10
+ def streams_secret=: (String?) -> void
11
+ def jwt_secret: () -> String?
12
+ def jwt_secret=: (String?) -> void
13
+ def jwt_ttl: () -> Integer?
14
+ def jwt_ttl=: (Integer) -> void
15
+ def broadcast_key: () -> String?
16
+ def broadcast_key=: (String?) -> void
7
17
  def redis_url: () -> String
8
18
  def redis_url=: (String) -> void
9
19
  def redis_sentinels: () -> Array[String | Hash[untyped, untyped]]?
@@ -41,8 +51,8 @@ module AnyCable
41
51
  def http_health_port=: (Integer) -> void
42
52
  def http_health_path: () -> String
43
53
  def http_health_path=: (String) -> void
44
- def http_rpc_secret: () -> String
45
- def http_rpc_secret=: (String) -> void
54
+ def http_rpc_secret: () -> String?
55
+ def http_rpc_secret=: (String?) -> void
46
56
  def version_check_enabled: () -> bool
47
57
  def version_check_enabled=: (bool) -> void
48
58
  def version_check_enabled?: () -> bool
@@ -52,6 +62,9 @@ module AnyCable
52
62
  end
53
63
 
54
64
  class Config < Anyway::Config
65
+ BROADCAST_SECRET_PHRASE: String
66
+ HTTP_RPC_SECRET_PHRASE: String
67
+
55
68
  def self.usage: (String txt) -> void
56
69
  def self.usages: () -> Array[String]
57
70
 
@@ -60,6 +73,9 @@ module AnyCable
60
73
  alias debug? debug
61
74
  alias version_check_enabled? version_check_enabled
62
75
 
76
+ def http_rpc_secret!: () -> String?
77
+ def broadcast_key!: () -> String?
78
+
63
79
  def load: (*untyped) -> void
64
80
  def http_health_port_provided?: () -> bool
65
81
  def to_redis_params: () -> { url: String, sentinels: Array[untyped]?, ssl_params: Hash[Symbol, untyped]? }
@@ -74,5 +90,6 @@ module AnyCable
74
90
  def write_preset: (Symbol, untyped, preset: String) -> void
75
91
  def write_config_attr: (Symbol, untyped) -> void
76
92
  def __trace__: () -> untyped
93
+ def infer_from_application_secret: (String) -> String?
77
94
  end
78
95
  end
@@ -0,0 +1,21 @@
1
+ module AnyCable
2
+ module JWT
3
+ interface _Impl
4
+ def encode: (Hash[String | Symbol, untyped] payload, String secret_key) -> String
5
+ def decode: (String token, String secret_key) -> Hash[String, untyped]
6
+ end
7
+
8
+ module BasicImpl
9
+ extend _Impl
10
+
11
+ def self.sign: (String data, String key) -> String
12
+ def self.secure_compare: (String a, String b) -> bool
13
+ end
14
+
15
+ def self.jwt_impl: () -> _Impl
16
+ def self.jwt_impl=: (_Impl) -> void
17
+
18
+ def self.encode: (untyped payload, ?secret_key: String?, ?ttl: Integer, ?expires_at: Time | Integer) -> String
19
+ def self.decode: (String token, ?secret_key: String?) -> untyped
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module AnyCable
2
+ interface _ObjectSerializer
3
+ def serialize: (untyped) -> untyped
4
+ def deserialize: (String) -> untyped
5
+ end
6
+
7
+ module Serializer
8
+ def self.object_serializer: () -> _ObjectSerializer?
9
+ def self.object_serializer=: (_ObjectSerializer?) -> void
10
+
11
+ def self.serialize: (untyped) -> untyped
12
+ def self.deserialize: (untyped) -> untyped
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module AnyCable
2
+ module Streams
3
+ def self.signed: (String) -> String
4
+ def self.verified: (String) -> String?
5
+
6
+ private def self.signature: (String) -> String
7
+ end
8
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.4
4
+ version: 1.5.0.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: google-protobuf
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '3.25'
33
+ version: '3.13'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '3.25'
40
+ version: '3.13'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: redis
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -217,6 +217,7 @@ files:
217
217
  - lib/anycable/grpc_kit/server.rb
218
218
  - lib/anycable/health_server.rb
219
219
  - lib/anycable/httrpc/server.rb
220
+ - lib/anycable/jwt.rb
220
221
  - lib/anycable/middleware.rb
221
222
  - lib/anycable/middleware_chain.rb
222
223
  - lib/anycable/middlewares/check_version.rb
@@ -230,7 +231,9 @@ files:
230
231
  - lib/anycable/rpc/handlers/disconnect.rb
231
232
  - lib/anycable/rspec.rb
232
233
  - lib/anycable/rspec/rpc_command_context.rb
234
+ - lib/anycable/serializer.rb
233
235
  - lib/anycable/socket.rb
236
+ - lib/anycable/streams.rb
234
237
  - lib/anycable/version.rb
235
238
  - sig/anycable.rbs
236
239
  - sig/anycable/broadcast_adapters.rbs
@@ -248,6 +251,7 @@ files:
248
251
  - sig/anycable/grpc/server.rbs
249
252
  - sig/anycable/health_server.rbs
250
253
  - sig/anycable/httrpc/server.rbs
254
+ - sig/anycable/jwt.rbs
251
255
  - sig/anycable/middleware.rbs
252
256
  - sig/anycable/middleware_chain.rbs
253
257
  - sig/anycable/middlewares/check_version.rbs
@@ -258,7 +262,9 @@ files:
258
262
  - sig/anycable/rpc/handlers/command.rbs
259
263
  - sig/anycable/rpc/handlers/connect.rbs
260
264
  - sig/anycable/rpc/handlers/disconnect.rbs
265
+ - sig/anycable/serializer.rbs
261
266
  - sig/anycable/socket.rbs
267
+ - sig/anycable/streams.rbs
262
268
  - sig/anycable/version.rbs
263
269
  - sig/manifest.yml
264
270
  homepage: http://github.com/anycable/anycable
@@ -282,9 +288,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
282
288
  version: 2.7.0
283
289
  required_rubygems_version: !ruby/object:Gem::Requirement
284
290
  requirements:
285
- - - ">="
291
+ - - ">"
286
292
  - !ruby/object:Gem::Version
287
- version: '0'
293
+ version: 1.3.1
288
294
  requirements: []
289
295
  rubygems_version: 3.4.20
290
296
  signing_key: