anycable-core 1.4.3 → 1.5.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a7b2ae33dc506f328c7ce62c3c4c80eabb17ed36ca4826808caeb0534a8b2ab
4
- data.tar.gz: 2cfae2f0b55718269c33dfc5877d55a2d210d9fa99f0aca85c2899720c0cf350
3
+ metadata.gz: b3469966952c29b422b3177acbb3402fc9bf500786d55774f9b3311b904548bb
4
+ data.tar.gz: 7e5a7302b61c07af90f61b11878212a464ba1eac0f7d6b664f91b8dcbd3fe9e6
5
5
  SHA512:
6
- metadata.gz: b10e485726fd3f48791883af759cfdbeb963a59798a3df7a5100e993891fdb8faff370d028f6b81010be79b2737405196e7f4988041c92896326b222eab4296a
7
- data.tar.gz: a901c3958ed1d866a6d7d7f20fb7e8ede84bfabbba29136a022e8eaf3241d10aa437d02c163fa54d0fe7aad8fef701df2299968a20fc887b9348276bee11098a
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.3"
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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 1.5.0.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-16 00:00:00.000000000 Z
11
+ date: 2024-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anyway_config
@@ -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,11 +288,11 @@ 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
- rubygems_version: 3.4.8
295
+ rubygems_version: 3.4.20
290
296
  signing_key:
291
297
  specification_version: 4
292
298
  summary: AnyCable core RPC implementation