async-htty 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7243a9a99e7a0957ae6a2641c13597899ab85210c5f5334aefc23c9d573681f3
4
+ data.tar.gz: 5022a69037b5ec3100456de037dd263f28995a135222bde4e2a8a721f2c2e0a4
5
+ SHA512:
6
+ metadata.gz: 8c9bbb53d42a71efe71c714cb4c6aa743c4e0f8fbefd60e4dec918a2e21d9123627a27a3994da9be17901314f684d4f3a942a6ce7caabf8092168afc9b6a2cd1
7
+ data.tar.gz: b07dbf1b26da9cbd084fd7a048dc7aa7be7122f26a8190da24fb4b4bb2a2f7f472419c508f91d2797035a26e12ef516d7b693b099b7537d6e441fb4de4df44e0
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async"
7
+ require "async/htty"
8
+ require "protocol/http/response"
9
+
10
+ app = Protocol::HTTP::Middleware.for do |request|
11
+ body = <<~HTML
12
+ <!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
17
+ <title>HTTY Browser Demo</title>
18
+ <style>
19
+ :root {
20
+ color-scheme: dark;
21
+ font-family: "Avenir Next", "Helvetica Neue", sans-serif;
22
+ }
23
+
24
+ body {
25
+ margin: 0;
26
+ min-height: 100vh;
27
+ padding: 32px;
28
+ background:
29
+ radial-gradient(circle at top left, rgba(253, 160, 133, 0.28), transparent 24%),
30
+ radial-gradient(circle at top right, rgba(246, 211, 101, 0.2), transparent 22%),
31
+ linear-gradient(180deg, #0b1220 0%, #08101b 100%);
32
+ color: #ecf3ff;
33
+ }
34
+
35
+ main {
36
+ max-width: 760px;
37
+ padding: 28px;
38
+ border-radius: 28px;
39
+ background: rgba(9, 17, 31, 0.82);
40
+ border: 1px solid rgba(148, 163, 184, 0.18);
41
+ backdrop-filter: blur(24px);
42
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
43
+ }
44
+
45
+ p {
46
+ color: #9fb0c8;
47
+ line-height: 1.6;
48
+ }
49
+
50
+ .badge {
51
+ display: inline-flex;
52
+ padding: 6px 10px;
53
+ border-radius: 999px;
54
+ background: rgba(246, 211, 101, 0.14);
55
+ color: #f6d365;
56
+ letter-spacing: 0.12em;
57
+ text-transform: uppercase;
58
+ font-size: 12px;
59
+ }
60
+
61
+ .card {
62
+ margin-top: 24px;
63
+ padding: 18px;
64
+ border-radius: 18px;
65
+ background: rgba(5, 11, 21, 0.92);
66
+ border: 1px solid rgba(148, 163, 184, 0.14);
67
+ }
68
+ </style>
69
+ </head>
70
+ <body>
71
+ <main>
72
+ <div class="badge">HTTY Browser Demo</div>
73
+ <h1>Attached browser surface over a normal terminal session.</h1>
74
+ <p>This page is served by a plain HTTP/2 application running inside the command process. Chimera keeps the terminal visible and mounts this document alongside it once the attached HTTP/2 connection has been established.</p>
75
+ <div class="card">
76
+ <strong>Request:</strong> #{request.method} #{request.path}
77
+ </div>
78
+ </main>
79
+ </body>
80
+ </html>
81
+ HTML
82
+
83
+ Protocol::HTTP::Response[200, [["content-type", "text/html; charset=utf-8"]], [body]]
84
+ end
85
+
86
+ Async::HTTY::Server.open(app)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async"
7
+ require "async/htty"
8
+ require "protocol/http/response"
9
+
10
+ app = Protocol::HTTP::Middleware.for do |request|
11
+ Protocol::HTTP::Response[200, [["content-type", "text/plain"]], ["Hello World from HTTY\n"]]
12
+ end
13
+
14
+ Async::HTTY::Server.open(app)
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "io/console"
7
+
8
+ unless STDIN.tty? && STDOUT.tty?
9
+ warn "This program must be run in an interactive terminal."
10
+ exit 1
11
+ end
12
+
13
+ original_state = `stty -g`.chomp
14
+
15
+ restore_terminal = proc do
16
+ next if original_state.empty?
17
+ system("stty", original_state, exception: false)
18
+ end
19
+
20
+ Signal.trap("TERM") do
21
+ puts "\nReceived TERM, restoring terminal state..."
22
+ restore_terminal.call
23
+ exit 0
24
+ end
25
+
26
+ Signal.trap("INT") do
27
+ puts "\nReceived INT, restoring terminal state..."
28
+ restore_terminal.call
29
+ exit 0
30
+ end
31
+
32
+ at_exit do
33
+ restore_terminal.call
34
+ end
35
+
36
+ puts <<~TEXT
37
+ Entering raw mode.
38
+ PID: #{Process.pid}
39
+
40
+ Controls:
41
+ - Press `q` to exit normally.
42
+ - Press Ctrl-C to send the raw byte 0x03; this script will exit and restore the terminal.
43
+ - From another shell, run `kill -TERM #{Process.pid}` to test normal signal cleanup.
44
+ - From another shell, run `kill -9 #{Process.pid}` to test abrupt termination.
45
+
46
+ While raw mode is active, keypresses are printed immediately as bytes.
47
+ TEXT
48
+
49
+ STDIN.raw(min: 0, time: 1) do |input|
50
+ loop do
51
+ chunk = input.read_nonblock(32, exception: false)
52
+
53
+ case chunk
54
+ when :wait_readable, nil
55
+ # Keep the process alive while waiting for external signals.
56
+ next
57
+ else
58
+ bytes = chunk.bytes
59
+ puts "read #{bytes.length} byte(s): #{bytes.map {|byte| format("0x%02X", byte)}.join(" ")}"
60
+
61
+ break if chunk.include?("q")
62
+ break if bytes.include?(3)
63
+ end
64
+ end
65
+ puts "Leaving raw mode normally."
66
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Async
7
+ module HTTY
8
+ class Error < StandardError
9
+ end
10
+
11
+ class UnsupportedError < Error
12
+ end
13
+
14
+ class DisabledError < UnsupportedError
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "protocol/htty"
7
+
8
+ require "async/http/protocol/http2"
9
+
10
+ module Async
11
+ module HTTY
12
+ module Protocol
13
+ module HTTY
14
+ def self.client(stream, settings: ::Async::HTTP::Protocol::HTTP2::CLIENT_SETTINGS)
15
+ stream = ::Protocol::HTTY::Stream.open(stream, bootstrap: :read)
16
+
17
+ client = ::Async::HTTP::Protocol::HTTP2::Client.new(stream)
18
+ client.send_connection_preface(settings)
19
+ client.start_connection
20
+
21
+ return client
22
+ end
23
+
24
+ def self.server(stream, settings: ::Async::HTTP::Protocol::HTTP2::SERVER_SETTINGS)
25
+ stream = ::Protocol::HTTY::Stream.open(stream, bootstrap: :write)
26
+
27
+ server = ::Async::HTTP::Protocol::HTTP2::Server.new(stream)
28
+ server.read_connection_preface(settings)
29
+ server.start_connection
30
+
31
+ return server
32
+ end
33
+
34
+ def self.names
35
+ ["htty"]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "protocol/htty"
7
+
8
+ module Async
9
+ module HTTY
10
+ module Protocol
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async"
7
+ require "io/console"
8
+ require "protocol/http/middleware"
9
+
10
+ require_relative "error"
11
+ require_relative "protocol"
12
+
13
+ module Async
14
+ module HTTY
15
+ class Server < ::Protocol::HTTP::Middleware
16
+ def self.for(**options, &block)
17
+ self.new(block, **options)
18
+ end
19
+
20
+ def self.with_raw_terminal(input, &block)
21
+ if input.respond_to?(:tty?) && input.tty? && input.respond_to?(:raw)
22
+ input.raw(&block)
23
+ else
24
+ block.call
25
+ end
26
+ end
27
+
28
+
29
+ def self.open(app = nil, input: $stdin, output: $stdout, env: ENV, **options, &block)
30
+ app ||= block
31
+ server = self.new(app, **options)
32
+
33
+ case env["HTTY"]
34
+ when "0"
35
+ raise DisabledError, "HTTY is disabled!"
36
+ when nil
37
+ $stderr.puts "HTTY is not supported by this environment, visit https://htty.dev for more information."
38
+ raise UnsupportedError, "HTTY is not supported by this environment"
39
+ end
40
+
41
+ Sync do |task|
42
+ with_raw_terminal(input) do
43
+ stream = ::IO::Stream::Duplex(input, output)
44
+ server.accept(stream, task: task)
45
+ end
46
+ end
47
+ end
48
+
49
+ def initialize(app, protocol: Protocol::HTTY)
50
+ super(app)
51
+ @protocol = protocol
52
+ end
53
+
54
+ attr :protocol
55
+
56
+ def accept(stream, task: ::Async::Task.current)
57
+ connection = @protocol.server(stream)
58
+
59
+ connection.each do |request|
60
+ self.call(request)
61
+ end
62
+
63
+ Array(task.children).each(&:wait)
64
+ ensure
65
+ if connection and !connection.closed?
66
+ connection.send_goaway
67
+ connection.close
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Async
7
+ module HTTY
8
+ VERSION = "0.1.0"
9
+ end
10
+ end
data/lib/async/htty.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "htty/version"
7
+ require_relative "htty/error"
8
+ require_relative "htty/protocol"
9
+ require_relative "htty/server"
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2026, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,51 @@
1
+ # Async::HTTY
2
+
3
+ `async-htty` adapts `Protocol::HTTY::Stream` into an Async-compatible HTTP/2 connection so terminal sessions can bootstrap HTTY and host `Protocol::HTTP::Middleware` applications over the resulting raw byte stream.
4
+
5
+ [![Development Status](https://github.com/socketry/async-htty/workflows/Test/badge.svg)](https://github.com/socketry/async-htty/actions?workflow=Test)
6
+
7
+ ## Usage
8
+
9
+ Please see the [project documentation](https://socketry.github.io/async-htty/) for more details.
10
+
11
+ - [Getting Started](https://socketry.github.io/async-htty/guides/getting-started/index) - This guide explains how to get started with `async-htty` as an Async-compatible runtime for HTTY sessions carrying plaintext HTTP/2 (`h2c`) over a DCS-bootstrapped raw terminal transport.
12
+
13
+ ## Releases
14
+
15
+ Please see the [project releases](https://socketry.github.io/async-htty/releases/index) for all releases.
16
+
17
+ ### v0.1.0
18
+
19
+ ## Contributing
20
+
21
+ We welcome contributions to this project.
22
+
23
+ 1. Fork it.
24
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
25
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
26
+ 4. Push to the branch (`git push origin my-new-feature`).
27
+ 5. Create new Pull Request.
28
+
29
+ ### Running Tests
30
+
31
+ To run the test suite:
32
+
33
+ ``` shell
34
+ bundle exec sus
35
+ ```
36
+
37
+ ### Making Releases
38
+
39
+ To make a new release:
40
+
41
+ ``` shell
42
+ bundle exec bake gem:release:patch # or minor or major
43
+ ```
44
+
45
+ ### Developer Certificate of Origin
46
+
47
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
48
+
49
+ ### Community Guidelines
50
+
51
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data/releases.md ADDED
@@ -0,0 +1,3 @@
1
+ # Releases
2
+
3
+ ## v0.1.0
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async"
7
+ require "protocol/http/request"
8
+ require "protocol/http/response"
9
+ require "protocol/http2"
10
+ require "async/htty"
11
+
12
+ describe Async::HTTY::Protocol::HTTY do
13
+ def make_pipes
14
+ server_input, client_output = IO.pipe
15
+ client_input, server_output = IO.pipe
16
+
17
+ {
18
+ server_input: server_input,
19
+ server_output: server_output,
20
+ client_input: client_input,
21
+ client_output: client_output,
22
+ }
23
+ end
24
+
25
+ def close_pipes(pipes)
26
+ pipes.each_value do |io|
27
+ io.close rescue nil
28
+ end
29
+ end
30
+
31
+ def client_stream(pipes)
32
+ IO::Stream::Duplex(pipes[:client_input], pipes[:client_output])
33
+ end
34
+
35
+ def server_stream(pipes)
36
+ IO::Stream::Duplex(pipes[:server_input], pipes[:server_output])
37
+ end
38
+
39
+ it "can carry an HTTP/2 request over HTTY bootstrap and raw transport" do
40
+ pipes = make_pipes
41
+
42
+ server = Async::HTTY::Server.for do |request|
43
+ Protocol::HTTP::Response[200, {}, ["Hello World"]]
44
+ end
45
+
46
+ Sync do |task|
47
+ server_task = task.async do
48
+ server.accept(server_stream(pipes))
49
+ end
50
+
51
+ client = subject.client(client_stream(pipes))
52
+ response = client.call(Protocol::HTTP::Request["GET", "/"])
53
+
54
+ expect(response.status).to be == 200
55
+ expect(response.read).to be == "Hello World"
56
+ ensure
57
+ client&.close
58
+ server_task&.stop
59
+ close_pipes(pipes)
60
+ end
61
+ end
62
+
63
+ it "ignores terminal noise before the HTTY bootstrap" do
64
+ pipes = make_pipes
65
+
66
+ Sync do |task|
67
+ pipes[:server_output].write("terminal noise\eP+reset:test\e\\")
68
+ pipes[:server_output].flush
69
+
70
+ server_task = task.async do
71
+ subject.server(server_stream(pipes))
72
+ end
73
+
74
+ client = subject.client(client_stream(pipes))
75
+
76
+ expect(client).not.to be(:closed?)
77
+ ensure
78
+ client&.close
79
+ server_task&.stop
80
+ close_pipes(pipes)
81
+ end
82
+ end
83
+
84
+ it "treats command exit after bootstrap without GOAWAY as an abort" do
85
+ pipes = make_pipes
86
+
87
+ pipes[:server_output].write("\eP+Hraw\e\\")
88
+ pipes[:server_output].close
89
+
90
+ Sync do
91
+ expect do
92
+ subject.client(client_stream(pipes))
93
+ end.to raise_exception(EOFError)
94
+ ensure
95
+ close_pipes(pipes)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "stringio"
7
+ require "protocol/http/middleware"
8
+ require "async/htty"
9
+
10
+ describe Async::HTTY::Server do
11
+ let(:server) {subject.new(Protocol::HTTP::Middleware::Okay)}
12
+ let(:env) {{"HTTY" => "1"}}
13
+
14
+ it "exposes the HTTY protocol by default" do
15
+ expect(server.protocol).to be == Async::HTTY::Protocol::HTTY
16
+ end
17
+
18
+ it "switches tty input into raw mode while accepting a session" do
19
+ input = Object.new
20
+ output = StringIO.new
21
+ connection = Object.new
22
+ protocol = Object.new
23
+ input.instance_variable_set(:@raw_called, false)
24
+
25
+ def input.tty?
26
+ true
27
+ end
28
+
29
+ def input.raw
30
+ @raw_called = true
31
+ yield
32
+ end
33
+
34
+ def input.raw_called?
35
+ @raw_called
36
+ end
37
+
38
+ def input.timeout
39
+ end
40
+
41
+ def connection.each
42
+ end
43
+
44
+ def connection.closed?
45
+ false
46
+ end
47
+
48
+ def connection.send_goaway
49
+ end
50
+
51
+ def connection.close
52
+ end
53
+
54
+ protocol.define_singleton_method(:server) do |stream|
55
+ connection
56
+ end
57
+
58
+ subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
59
+
60
+ expect(input.raw_called?).to be == true
61
+ expect(output.string).to be == ""
62
+ end
63
+
64
+ it "leaves raw mode if protocol setup fails" do
65
+ input = Object.new
66
+ output = StringIO.new
67
+ protocol = Object.new
68
+ input.instance_variable_set(:@raw_exited, false)
69
+
70
+ def input.tty?
71
+ true
72
+ end
73
+
74
+ def input.raw
75
+ yield
76
+ ensure
77
+ @raw_exited = true
78
+ end
79
+
80
+ def input.raw_exited?
81
+ @raw_exited
82
+ end
83
+
84
+ def input.timeout
85
+ end
86
+
87
+ protocol.define_singleton_method(:server) do |stream|
88
+ raise EOFError, "aborted"
89
+ end
90
+
91
+ expect do
92
+ subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
93
+ end.to raise_exception(EOFError, message: be =~ /aborted/)
94
+
95
+ expect(input.raw_exited?).to be == true
96
+ end
97
+
98
+ it "sends command-side GOAWAY before closing the connection" do
99
+ input = StringIO.new
100
+ output = StringIO.new
101
+ connection = Object.new
102
+ protocol = Object.new
103
+ events = []
104
+
105
+ def connection.each
106
+ end
107
+
108
+ connection.define_singleton_method(:closed?) do
109
+ false
110
+ end
111
+
112
+ connection.define_singleton_method(:send_goaway) do
113
+ events << :goaway
114
+ end
115
+
116
+ connection.define_singleton_method(:close) do
117
+ events << :close
118
+ end
119
+
120
+ protocol.define_singleton_method(:server) do |stream|
121
+ connection
122
+ end
123
+
124
+ subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
125
+
126
+ expect(events).to be == [:goaway, :close]
127
+ end
128
+
129
+ it "opens a server with default stdio-style arguments" do
130
+ input = StringIO.new
131
+ output = StringIO.new
132
+ accepted = false
133
+
134
+ server = Object.new
135
+ server.define_singleton_method(:each) do
136
+ end
137
+ server.define_singleton_method(:closed?) do
138
+ false
139
+ end
140
+ server.define_singleton_method(:send_goaway) do
141
+ end
142
+ server.define_singleton_method(:close) do
143
+ end
144
+
145
+ protocol = Object.new
146
+ protocol.define_singleton_method(:server) do |stream|
147
+ accepted = true
148
+ server
149
+ end
150
+
151
+ subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
152
+
153
+ expect(accepted).to be == true
154
+ expect(output.string).to be == ""
155
+ end
156
+
157
+ it "raises a typed error when HTTY is disabled" do
158
+ expect do
159
+ subject.open(Protocol::HTTP::Middleware::Okay, input: StringIO.new, output: StringIO.new, env: {"HTTY" => "0"})
160
+ end.to raise_exception(Async::HTTY::DisabledError, message: be =~ /disabled/)
161
+
162
+ expect(Async::HTTY::DisabledError).to be < Async::HTTY::UnsupportedError
163
+ end
164
+
165
+ it "prints help and raises a typed error when HTTY is not advertised" do
166
+ error_output = StringIO.new
167
+ original_stderr = $stderr
168
+
169
+ begin
170
+ $stderr = error_output
171
+
172
+ expect do
173
+ subject.open(Protocol::HTTP::Middleware::Okay, input: StringIO.new, output: StringIO.new, env: {})
174
+ end.to raise_exception(Async::HTTY::UnsupportedError, message: be =~ /not supported/)
175
+ ensure
176
+ $stderr = original_stderr
177
+ end
178
+
179
+ expect(error_output.string).to be(:include?, "https://htty.dev")
180
+ end
181
+
182
+ it "opens a server within its own async context when no task is provided" do
183
+ input = StringIO.new
184
+ output = StringIO.new
185
+ accepted = false
186
+
187
+ server = Object.new
188
+ server.define_singleton_method(:each) do
189
+ end
190
+ server.define_singleton_method(:closed?) do
191
+ false
192
+ end
193
+ server.define_singleton_method(:send_goaway) do
194
+ end
195
+ server.define_singleton_method(:close) do
196
+ end
197
+
198
+ protocol = Object.new
199
+ protocol.define_singleton_method(:server) do |stream|
200
+ accepted = true
201
+ server
202
+ end
203
+
204
+ subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
205
+
206
+ expect(accepted).to be == true
207
+ end
208
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/htty/version"
7
+
8
+ describe Async::HTTY do
9
+ it "has a version number" do
10
+ expect(Async::HTTY::VERSION).to be =~ /\d+\.\d+\.\d+/
11
+ end
12
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async-htty
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
13
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
14
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
15
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
16
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
17
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
18
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
19
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
20
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
21
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
22
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
23
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
24
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
25
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
26
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
27
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
28
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
30
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
31
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
32
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
33
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
34
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
35
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
36
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
+ -----END CERTIFICATE-----
39
+ date: 1980-01-02 00:00:00.000000000 Z
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: async
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.39'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.39'
55
+ - !ruby/object:Gem::Dependency
56
+ name: async-http
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.88'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.88'
69
+ - !ruby/object:Gem::Dependency
70
+ name: protocol-http
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.62'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.62'
83
+ - !ruby/object:Gem::Dependency
84
+ name: protocol-htty
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.2'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.2'
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - examples/browser_demo.rb
102
+ - examples/hello_world.rb
103
+ - examples/raw_mode_probe.rb
104
+ - lib/async/htty.rb
105
+ - lib/async/htty/error.rb
106
+ - lib/async/htty/protocol.rb
107
+ - lib/async/htty/protocol/htty.rb
108
+ - lib/async/htty/server.rb
109
+ - lib/async/htty/version.rb
110
+ - license.md
111
+ - readme.md
112
+ - releases.md
113
+ - test/async/htty.rb
114
+ - test/async/htty/protocol/htty.rb
115
+ - test/async/htty/server.rb
116
+ homepage: https://github.com/socketry/async-htty
117
+ licenses:
118
+ - MIT
119
+ metadata:
120
+ documentation_uri: https://socketry.github.io/async-htty/
121
+ source_code_uri: https://github.com/socketry/async-htty.git
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '3.3'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 4.0.6
137
+ specification_version: 4
138
+ summary: An Async server runtime for HTTY sessions that carry HTTP/2 over terminal
139
+ side channels.
140
+ test_files: []
metadata.gz.sig ADDED
Binary file