terminalwire 0.3.5.alpha2 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c61087330d6c3ad29504c44b4ee3ac3d3a49b481f4ba0e42aa889017a125d8b
4
- data.tar.gz: 9a8f93609cf71329b003d3f995e48caf77b73bf8b1097fb6cce433bd9868cbc9
3
+ metadata.gz: 3a3b1a0d008654c84320fb1300e080d289e6b620b1526556427b8bdf493286dc
4
+ data.tar.gz: 0ef9c8b606d22d79d44e258ae2d38fc19a4cfcb18d92ce106b0e3fc33806e408
5
5
  SHA512:
6
- metadata.gz: '0306186b973039130168de5a56c89931e139d34ff3a83c7e32a11bf31d984c44253bd2b3a8c52752539861106033acabbe54502ad935537c02a07213a956da33'
7
- data.tar.gz: 1af32080f60c605f682fd0e671f1f1484d12492b4e828e16a581469589ffc87fdc70cf0f0609110bef84437eb805e0057f6749ce18c2b6842afef886dd447b39
6
+ metadata.gz: 75d87cd218444227f5e1a5c17e65ed887e28992810dfe249050e22b159b636b77a40fca4fbf3346f60467526a4c4603060e67f0819c1ff9e4afa1ff105246417
7
+ data.tar.gz: 613575a6abb5e75498be37aaaf94e970997545d780dce942293c129d5b99848e18d9cd0757f738481d38efb09d903533c5eb10e833714797b93196c9f1c4afed
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "msgpack"
4
+
5
+ module Terminalwire::V2
6
+ # Pure bytes <-> frame conversion. A frame is a Hash with string keys (the wire
7
+ # shape). No I/O, no transport — this is the sans-IO seam the conformance corpus
8
+ # exercises directly.
9
+ module Codec
10
+ # Largest valid stream id: a signed 64-bit max. Go decodes sids into int64, so
11
+ # anything above this would wrap to a negative (colliding) sid there; bounding
12
+ # it here keeps all three impls' sid validity identical.
13
+ MAX_SID = (1 << 63) - 1
14
+
15
+ module_function
16
+
17
+ # @param frame [Hash] a frame with string keys
18
+ # @return [String] MessagePack bytes (binary encoding)
19
+ def encode(frame)
20
+ raise ProtocolError, "frame must be a Hash, got #{frame.class}" unless frame.is_a?(Hash)
21
+
22
+ MessagePack.pack(frame)
23
+ end
24
+
25
+ # @param bytes [String] MessagePack bytes for exactly one frame
26
+ # @return [Hash] the decoded frame with string keys
27
+ # @raise [ProtocolError] if the bytes are not a well-formed frame
28
+ def decode(bytes)
29
+ obj =
30
+ begin
31
+ MessagePack.unpack(bytes)
32
+ rescue StandardError => e
33
+ raise ProtocolError, "malformed msgpack: #{e.message}"
34
+ end
35
+
36
+ raise ProtocolError, "frame must be a map" unless obj.is_a?(Hash)
37
+ # 't' must be a NON-EMPTY type string. Go and Elixir reject "" at the codec;
38
+ # Ruby used to let it through to the state machine. An empty type is not a
39
+ # valid frame — reject it here so all three behave identically.
40
+ raise ProtocolError, "frame missing string 't'" unless obj["t"].is_a?(String) && !obj["t"].empty?
41
+ # 'sid' must be a non-negative integer that fits in a signed 64-bit int (see
42
+ # MAX_SID): real sids are small and server-allocated, and the range bound keeps
43
+ # the three impls aligned (Go would otherwise wrap a uint64 sid to a negative).
44
+ sid = obj["sid"]
45
+ unless sid.is_a?(Integer) && sid >= 0 && sid <= MAX_SID
46
+ raise ProtocolError, "frame missing integer 'sid'"
47
+ end
48
+
49
+ obj
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+ require "base64"
6
+ require "pathname"
7
+
8
+ module Terminalwire::V2
9
+ # Loads the language-neutral conformance corpus and resolves its typed sentinels
10
+ # into native Ruby values. The Go and Elixir runners do the equivalent. This is
11
+ # what lets one corpus validate every implementation.
12
+ #
13
+ # Two file shapes live in the corpus:
14
+ # * simple vector tables (negotiate/roundtrip/golden/validate/flow) — YAML/JSON,
15
+ # with `$bin` base64 + `bytes_hex` sentinels;
16
+ # * "tapes" (session) — S-expressions (see Sexp): recorded client<->server
17
+ # interactions. Sexp is used for the tapes because it is trivial and
18
+ # unambiguous to parse in every language and reads like a transcript.
19
+ module Conformance
20
+ module_function
21
+
22
+ def root
23
+ Pathname.new(ENV.fetch("TERMINALWIRE_CORPUS") do
24
+ File.expand_path("../../../conformance", __dir__)
25
+ end)
26
+ end
27
+
28
+ def vectors_dir
29
+ root.join("vectors")
30
+ end
31
+
32
+ # Load every vector file in a category, dispatching by extension, and return a
33
+ # flat array of cases with sentinels resolved to native values. Fails LOUDLY
34
+ # when the corpus is absent rather than silently running zero cases.
35
+ def load(category)
36
+ unless vectors_dir.directory?
37
+ raise "conformance corpus not found at #{vectors_dir} — set TERMINALWIRE_CORPUS " \
38
+ "to the corpus directory (it ships in terminalwire/protocol). Without it " \
39
+ "the corpus specs would silently run zero cases."
40
+ end
41
+ Dir.glob(vectors_dir.join(category, "*.{yml,yaml,json,sexp}")).sort.flat_map do |path|
42
+ case File.extname(path)
43
+ when ".sexp" then Sexp.load(File.read(path)) # tapes (bin already resolved)
44
+ when ".json" then resolve(JSON.parse(File.read(path)))
45
+ else resolve(YAML.safe_load_file(path))
46
+ end
47
+ end
48
+ end
49
+
50
+ # Recursively resolve { "$bin" => base64 } sentinels into binary strings.
51
+ def resolve(value)
52
+ case value
53
+ when Hash
54
+ if value.size == 1 && value.key?("$bin")
55
+ Base64.decode64(value.fetch("$bin")).b
56
+ else
57
+ value.transform_values { |v| resolve(v) }
58
+ end
59
+ when Array
60
+ value.map { |v| resolve(v) }
61
+ else
62
+ value
63
+ end
64
+ end
65
+
66
+ # "a1 74 ff" -> binary string
67
+ def hex_to_bytes(hex)
68
+ hex.split.map { |byte| Integer(byte, 16) }.pack("C*")
69
+ end
70
+
71
+ # A tiny S-expression reader + tape interpreter. The whole grammar:
72
+ # list = "(" form* ")" ; atom = token | "string" | :keyword | number | true|false|nil ; ; comment
73
+ # Data mapping (unambiguous by each list's shape):
74
+ # (type :k v ...) FRAME -> {"t"=>type, k=>v} (:k v ...) MAP -> {k=>v}
75
+ # (a b c) LIST -> [a,b,c] (bin "b64") -> bytes
76
+ # A tape file is (tape NAME (ROLE ...config) <transcript>...). The transcript is
77
+ # flat and reads like a recording; #interpret groups it into the step shape the
78
+ # runners consume ({recv|do, emit:[...], reject} / {process, out:[...], exit, stdout}).
79
+ module Sexp
80
+ module_function
81
+
82
+ def load(text)
83
+ read_all(text).map { |form| interpret(form) }
84
+ end
85
+
86
+ def read_all(text)
87
+ toks = tokenize(text)
88
+ forms = []
89
+ forms << read_form(toks) until toks.empty?
90
+ forms
91
+ end
92
+
93
+ def tokenize(str)
94
+ toks = []
95
+ i = 0
96
+ n = str.length
97
+ while i < n
98
+ c = str[i]
99
+ if c == ";"
100
+ i += 1 while i < n && str[i] != "\n"
101
+ elsif c =~ /\s/
102
+ i += 1
103
+ elsif c == "(" || c == ")"
104
+ toks << c
105
+ i += 1
106
+ elsif c == '"'
107
+ j = i + 1
108
+ buf = +""
109
+ while j < n && str[j] != '"'
110
+ if str[j] == "\\"
111
+ buf << { "n" => "\n", "t" => "\t", "r" => "\r" }.fetch(str[j + 1], str[j + 1])
112
+ j += 2
113
+ else
114
+ buf << str[j]
115
+ j += 1
116
+ end
117
+ end
118
+ toks << [:str, buf]
119
+ i = j + 1
120
+ else
121
+ j = i
122
+ j += 1 while j < n && !"() \t\r\n;\"".include?(str[j])
123
+ toks << [:tok, str[i...j]]
124
+ i = j
125
+ end
126
+ end
127
+ toks
128
+ end
129
+
130
+ def read_form(toks)
131
+ t = toks.shift
132
+ if t == "("
133
+ list = []
134
+ list << read_form(toks) until toks.first == ")"
135
+ toks.shift
136
+ list
137
+ elsif t.is_a?(Array) && t[0] == :str
138
+ t[1]
139
+ else
140
+ atom(t[1])
141
+ end
142
+ end
143
+
144
+ def atom(text)
145
+ case text
146
+ when "true" then true
147
+ when "false" then false
148
+ when "nil" then nil
149
+ else
150
+ if text.start_with?(":") then text[1..].to_sym
151
+ elsif text.match?(/\A-?\d+\z/) then text.to_i
152
+ elsif text.match?(/\A-?\d+\.\d+\z/) then text.to_f
153
+ else text
154
+ end
155
+ end
156
+ end
157
+
158
+ def value(form)
159
+ return form unless form.is_a?(Array)
160
+ return [] if form.empty?
161
+
162
+ head = form[0]
163
+ if head.is_a?(Symbol)
164
+ to_map(form)
165
+ elsif head == "bin"
166
+ Base64.decode64(form[1]).b
167
+ elsif form[1..].any? { |e| e.is_a?(Symbol) }
168
+ { "t" => head }.merge(to_map(form[1..]))
169
+ else
170
+ form.map { |e| value(e) }
171
+ end
172
+ end
173
+
174
+ def to_map(pairs)
175
+ h = {}
176
+ pairs.each_slice(2) { |k, v| h[k.to_s] = value(v) }
177
+ h
178
+ end
179
+
180
+ def interpret(form)
181
+ _tape, name, config_form, *transcript = form
182
+ role = config_form[0]
183
+ config = to_map(config_form[1..])
184
+ steps = role == "client" ? group_client(transcript) : group_server(transcript)
185
+ { "name" => name, "role" => role, "config" => config, "tape" => steps }
186
+ end
187
+
188
+ def group_server(forms)
189
+ steps = []
190
+ forms.each do |f|
191
+ case f[0]
192
+ when "recv" then steps << { "recv" => value(f[1]), "emit" => [] }
193
+ when "do" then steps << { "do" => action(f[1]), "emit" => [] }
194
+ when "send" then steps.last["emit"] << { "send" => value(f[1]) }
195
+ when "event"
196
+ ev = { "event" => f[1].to_s }
197
+ ev["data"] = to_map(f[2..]) unless f[2..].empty?
198
+ steps.last["emit"] << ev
199
+ when "reject" then steps.last["reject"] = true
200
+ end
201
+ end
202
+ steps
203
+ end
204
+
205
+ def group_client(forms)
206
+ steps = []
207
+ forms.each do |f|
208
+ case f[0]
209
+ when "process" then steps << { "process" => value(f[1]), "out" => [] }
210
+ when "out" then steps.last["out"] << value(f[1])
211
+ when "exit" then steps.last["exit"] = f[1]
212
+ when "stdout" then steps.last["stdout"] = f[1]
213
+ end
214
+ end
215
+ steps
216
+ end
217
+
218
+ def action(form)
219
+ head = form[0]
220
+ rest = form[1..]
221
+ if rest.length == 1 && !rest[0].is_a?(Symbol) && !rest[0].is_a?(Array)
222
+ { head => rest[0] }
223
+ else
224
+ { head => to_map(rest) }
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminalwire::V2
4
+ class Error < StandardError; end
5
+
6
+ # Raised into the CLI thread to deliver a client Ctrl-C, like a local SIGINT —
7
+ # but NOT a SignalException (Ruby's Interrupt). Raising the real Interrupt into a
8
+ # thread inside a Falcon worker disturbs the async reactor and kills the
9
+ # connection; a plain Exception subclass interrupts blocking calls without that.
10
+ # Subclasses Exception (not StandardError) so user CLI code's `rescue
11
+ # StandardError` can't swallow it — only the Handler catches it -> exit 130.
12
+ class Interrupted < Exception; end
13
+
14
+ # Raised when bytes off the wire are not a well-formed frame.
15
+ class ProtocolError < Error; end
16
+
17
+ # Raised on the server side when a `response` came back with ok: false.
18
+ class ResponseError < Error
19
+ attr_reader :code
20
+
21
+ def initialize(code, message)
22
+ @code = code
23
+ super("#{code}: #{message}")
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminalwire::V2
4
+ # Builders for each frame type. Keep frame construction in one place so the wire
5
+ # shape is defined once. All builders return a Hash with string keys.
6
+ module Frames
7
+ module_function
8
+
9
+ def hello(protocol:, capabilities:, program:, entitlement:, terminal: DEFAULT_TERMINAL, flow: DEFAULT_FLOW)
10
+ {
11
+ "t" => Protocol::Type::HELLO, "sid" => Protocol::CONTROL_SID,
12
+ "protocol" => protocol, "capabilities" => capabilities,
13
+ "program" => program, "entitlement" => entitlement,
14
+ "terminal" => terminal, "flow" => flow
15
+ }
16
+ end
17
+
18
+ # The client's initial flow-control offer: how many bytes of output it will
19
+ # accept per stream before the server must wait for a window_adjust.
20
+ DEFAULT_FLOW = { "window" => Protocol::DEFAULT_WINDOW }.freeze
21
+
22
+ # Client -> server: extend the output window for a stream by `bytes`.
23
+ def window_adjust(sid:, bytes:)
24
+ { "t" => Protocol::Type::WINDOW_ADJUST, "sid" => sid, "bytes" => bytes }
25
+ end
26
+
27
+ # The client's terminal at connect time (structured per TERMINAL.md: per-stream
28
+ # kinds + a device block); resize/mode frames update the device thereafter.
29
+ DEFAULT_TERMINAL = {
30
+ "stdin" => { "kind" => "tty" },
31
+ "stdout" => { "kind" => "tty" },
32
+ "stderr" => { "kind" => "tty" },
33
+ "device" => {
34
+ "cols" => 80, "rows" => 24, "xpixels" => 0, "ypixels" => 0,
35
+ "term" => "", "color" => "none", "encoding" => "UTF-8", "mode" => "cooked"
36
+ }
37
+ }.freeze
38
+
39
+ # Generic async terminal signal (client -> server). resize/interrupt are the
40
+ # named variants; collapsing them into one frame type keeps the protocol small.
41
+ def signal(name, payload = {})
42
+ { "t" => Protocol::Type::SIGNAL, "sid" => Protocol::CONTROL_SID, "name" => name }.merge(payload)
43
+ end
44
+
45
+ def resize(cols:, rows:)
46
+ signal(Protocol::Signal::RESIZE, { "cols" => cols, "rows" => rows })
47
+ end
48
+
49
+ def interrupt
50
+ signal(Protocol::Signal::INTERRUPT)
51
+ end
52
+
53
+ def welcome(protocol:, capabilities:)
54
+ {
55
+ "t" => Protocol::Type::WELCOME, "sid" => Protocol::CONTROL_SID,
56
+ "protocol" => protocol, "capabilities" => capabilities
57
+ }
58
+ end
59
+
60
+ def incompatible(supported:, message:)
61
+ # Normalize to string keys so every wire frame is uniformly string-keyed
62
+ # (the negotiator hands us a symbol-keyed Ruby hash).
63
+ min = supported[:min] || supported["min"]
64
+ max = supported[:max] || supported["max"]
65
+ {
66
+ "t" => Protocol::Type::INCOMPATIBLE, "sid" => Protocol::CONTROL_SID,
67
+ "supported" => { "min" => min, "max" => max }, "message" => message
68
+ }
69
+ end
70
+
71
+ def exit(status:)
72
+ { "t" => Protocol::Type::EXIT, "sid" => Protocol::CONTROL_SID, "status" => status }
73
+ end
74
+
75
+ def open(sid:, stream:, mode: nil)
76
+ frame = { "t" => Protocol::Type::OPEN, "sid" => sid, "stream" => stream }
77
+ frame["mode"] = mode if mode # input streams carry the line-discipline mode
78
+ frame
79
+ end
80
+
81
+ def data(sid:, bytes:)
82
+ { "t" => Protocol::Type::DATA, "sid" => sid, "bytes" => bytes.b }
83
+ end
84
+
85
+ def close(sid:)
86
+ { "t" => Protocol::Type::CLOSE, "sid" => sid }
87
+ end
88
+
89
+ def request(sid:, resource:, method:, params: {})
90
+ {
91
+ "t" => Protocol::Type::REQUEST, "sid" => sid,
92
+ "resource" => resource, "method" => method, "params" => params
93
+ }
94
+ end
95
+
96
+ def response_ok(sid:, value:)
97
+ { "t" => Protocol::Type::RESPONSE, "sid" => sid, "ok" => true, "value" => value }
98
+ end
99
+
100
+ def response_error(sid:, code:, message:)
101
+ {
102
+ "t" => Protocol::Type::RESPONSE, "sid" => sid, "ok" => false,
103
+ "error" => { "code" => code, "message" => message }
104
+ }
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminalwire::V2
4
+ # Allocates stream ids and correlates in-flight requests to their responses.
5
+ # The starting id is injectable so recorded vectors replay deterministically.
6
+ class Mux
7
+ def initialize(start: 1)
8
+ raise ArgumentError, "start must be >= 1 (0 is the control stream)" if start < 1
9
+
10
+ @next = start
11
+ @pending = {}
12
+ # The runtime allocates/registers from the caller thread while the read
13
+ # pump resolves from its own thread, so the registry is mutex-guarded.
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ # Allocate a fresh stream id.
18
+ def allocate
19
+ @mutex.synchronize do
20
+ sid = @next
21
+ @next += 1
22
+ sid
23
+ end
24
+ end
25
+
26
+ # Mark a request stream as awaiting a response, stashing caller context.
27
+ def register(sid, context = nil)
28
+ @mutex.synchronize { @pending[sid] = context }
29
+ end
30
+
31
+ def pending?(sid)
32
+ @mutex.synchronize { @pending.key?(sid) }
33
+ end
34
+
35
+ # Resolve a pending request, returning (and removing) its context.
36
+ def resolve(sid)
37
+ @mutex.synchronize do
38
+ raise ProtocolError, "response for unknown stream #{sid}" unless @pending.key?(sid)
39
+
40
+ @pending.delete(sid)
41
+ end
42
+ end
43
+
44
+ def pending_count
45
+ @mutex.synchronize { @pending.size }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminalwire::V2
4
+ # Pure handshake negotiation: given what the client speaks and what the server
5
+ # supports, decide the agreed protocol version and capability set. This is a
6
+ # function, not a state machine, so it is trivially testable and identical
7
+ # across languages (see conformance/vectors/negotiate).
8
+ module Negotiator
9
+ module_function
10
+
11
+ # @return [Hash] either
12
+ # { decision: "welcome", protocol: Integer, capabilities: Array<String> }
13
+ # or
14
+ # { decision: "incompatible", supported: { min:, max: } }
15
+ def negotiate(client_protocol:, client_capabilities:, server_min:, server_max:, server_capabilities:)
16
+ if client_protocol < server_min
17
+ return {
18
+ decision: "incompatible",
19
+ supported: { min: server_min, max: server_max }
20
+ }
21
+ end
22
+
23
+ agreed = [client_protocol, server_max].min
24
+ # Intersection, preserving the client's advertised order.
25
+ capabilities = client_capabilities & server_capabilities
26
+
27
+ { decision: "welcome", protocol: agreed, capabilities: capabilities }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminalwire::V2
4
+ # Wire-level constants for the v2 protocol. See ../../PROTOCOL.md.
5
+ module Protocol
6
+ # Frame protocol version this implementation speaks.
7
+ VERSION = 2
8
+
9
+ # Range of protocol versions this implementation can negotiate.
10
+ MIN_VERSION = 2
11
+ MAX_VERSION = 2
12
+
13
+ # The reserved control stream id.
14
+ CONTROL_SID = 0
15
+
16
+ # Capabilities advertised by a fully-featured client/server. Negotiation
17
+ # intersects the two sides' sets, so a peer only uses a feature the other
18
+ # advertises. ADD a capability here when you add an optional feature; that is
19
+ # the additive-change path (old peers simply won't list it, and the feature
20
+ # stays dormant for them).
21
+ #
22
+ # NOTE: negotiation records the intersection, but the server does NOT currently
23
+ # refuse a request() for a capability the client didn't advertise — Context
24
+ # issues file/directory/browser/env/terminal-query requests regardless, and the
25
+ # CLIENT is the enforcement point (it rejects an un-granted or un-negotiated op).
26
+ # Don't rely on this list as a server-side gate; it isn't one yet.
27
+ CAPABILITIES = %w[
28
+ stdio file directory browser env
29
+ signal flow raw-input terminal-query
30
+ ].freeze
31
+
32
+ # Output stream names (open.stream).
33
+ module Stream
34
+ STDOUT = "stdout"
35
+ STDERR = "stderr"
36
+ STDIN_RAW = "stdin-raw"
37
+ end
38
+
39
+ # Terminal line-discipline modes the client applies to its real terminal while
40
+ # a raw input stream / secret read is open (see ../../TERMINAL.md §5).
41
+ module Mode
42
+ COOKED = "cooked" # line editing + echo + signal keys (default)
43
+ NOECHO = "noecho" # cooked, echo off (passwords)
44
+ CBREAK = "cbreak" # char-at-a-time, echo ON, signal keys ON (single-key y/n)
45
+ RAW = "raw" # char-at-a-time, echo off, signals delivered as bytes (TUIs)
46
+ end
47
+
48
+ # Frame types.
49
+ module Type
50
+ HELLO = "hello"
51
+ WELCOME = "welcome"
52
+ INCOMPATIBLE = "incompatible"
53
+ EXIT = "exit"
54
+ OPEN = "open"
55
+ DATA = "data"
56
+ CLOSE = "close"
57
+ REQUEST = "request"
58
+ RESPONSE = "response"
59
+ SIGNAL = "signal"
60
+ WINDOW_ADJUST = "window_adjust"
61
+ end
62
+
63
+ # Names carried by a `signal` frame (client -> server, async terminal events).
64
+ module Signal
65
+ RESIZE = "resize"
66
+ INTERRUPT = "interrupt"
67
+ end
68
+
69
+ # Default initial per-output-stream flow-control window (bytes) the client
70
+ # grants the server. Must be >= the server's output chunk size so a single
71
+ # chunk can never exceed an empty window and deadlock.
72
+ DEFAULT_WINDOW = 256 * 1024
73
+
74
+ # Hard ceiling on a flow window (bytes). A window can never grow past this no
75
+ # matter what a peer offers or grants — the credit ledger clamps to it (see
76
+ # Window). This bounds how much output a server may buffer ahead of a slow or
77
+ # hostile client: without it, a client could offer/grant an enormous window and
78
+ # dissolve backpressure entirely, ballooning the server's transport buffers. 64×
79
+ # the default offer — ample for any terminal stream, far below a memory hazard.
80
+ MAX_WINDOW = 16 * 1024 * 1024
81
+
82
+ # Error codes carried on a `response` with ok: false.
83
+ module ErrorCode
84
+ DENIED = "denied"
85
+ NOT_FOUND = "not_found"
86
+ IO = "io"
87
+ PROTOCOL = "protocol"
88
+ INTERNAL = "internal"
89
+ end
90
+ end
91
+ end