confium 0.4.1 → 0.5.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: a46dd7e9a6967adabafbf136b1d8397b30dd6b62cf391bc65a3cbfbdb40aa974
4
- data.tar.gz: 4644e733341fedcdb25860ba0a11f158307f379067e1455415c6ca2f7fbb9cb5
3
+ metadata.gz: d09c11dbd19d2df0b80cac44a48013395a7d96a1430506be1b1dc42b0b3831a3
4
+ data.tar.gz: c15b846b399f57a4a36fe808a8e98e3e224ec3873690a81e43f7c281134b5918
5
5
  SHA512:
6
- metadata.gz: a11f941a43c841a1af5e64cd0529faf177fdcb30908843b5458ac75fa92ea9c48ab2ecf351f674d90e809f1b21af2c8c039a4c6263b6a916ad166dd7b28be7ce
7
- data.tar.gz: 2b6ef5346cf0edd3c956b99908f4d581589da7a4c0aad6dafbaf019482df392294acd7ae58ab54781dda6824b2f77858b93c7333f9a31432226c1725dd2fa4eb
6
+ metadata.gz: 568abf025728f288d453c8f315930665f3fabb77abc1484e743c7539fd36c45813c721c970202f40a90e4e34969313a28adca521b5a38cd62a01984743b92a74
7
+ data.tar.gz: 59877e7dc1b834e49e7f9c5b5a25e1a87bcd888f50a15bdc3917f6024685c168bc87feb4d30bc3f5f9cb37b232a3b141652d9e07b0501f9c2e110205486e7b3c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0] — 2026-08-24
4
+
5
+ ### Added
6
+
7
+ - **Ruby 4.0 support** — Windows ships an exact `4.0` ABI window;
8
+ every platform install-checks on Ruby 4.0; the test matrix gains
9
+ 4.0 legs. (There is no Ruby 3.5 — 4.0 is the successor line.)
10
+ - **Multi-host threshold signing**: `Confium::TC::NetworkCoordinator`
11
+ (TCP service, NDJSON + hex wire protocol) + `Confium::TC::SignerClient`
12
+ — signers on separate machines submit commitments and shares;
13
+ aggregation runs the real CMP20/GG18 combine and returns an
14
+ OpenSSL-verifiable signature. Plain TCP for loopback/private
15
+ networks; the upstream noise transport replaces it later.
16
+ - **OpenTelemetry export**: `Confium::Audit::OtlpSink` ships every
17
+ audit record to an OTLP collector over OTLP/HTTP JSON (logs
18
+ signal, stdlib-only, failures drop-and-report).
19
+ - `Confium::Composite::Signature#to_json` — instances remember their
20
+ source (components or a canonical JSON document) and re-serialize
21
+ losslessly, completing the JSON transport symmetry.
22
+
23
+ ### Fixed
24
+
25
+ - `Confium::TC::Coordinator` now aggregates REAL threshold-ECDSA
26
+ signatures: once the threshold is met, it runs the CMP20 or GG18
27
+ combine (selected per session via `scheme:`) and returns a 64-byte
28
+ signature verifiable under the quorum public key. Previously
29
+ `aggregate` concatenated the share bytes — a placeholder that
30
+ returned garbage. Unknown sessions below-threshold now raise
31
+ `Confium::ThresholdError`.
32
+ - `Confium::TC::Coordinator` and `ShareFile` were unreachable after
33
+ `require "confium"`: the TC namespace file that registers them
34
+ never loaded because the native extension pre-defines
35
+ `Confium::TC` (the same orphaned-module trap PKI had). The
36
+ namespace file is now eager-required; `TC::Session` stays lazy —
37
+ it wraps the engine via FFI and needs the external libconfium
38
+ dylib.
39
+
3
40
  ## [0.4.1] — 2026-08-24
4
41
 
5
42
  ### Fixed
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'time'
6
+
7
+ module Confium
8
+ module Audit
9
+ # Exports audit records to an OpenTelemetry Collector over
10
+ # OTLP/HTTP JSON (the logs signal — an audit event is a log
11
+ # record, not a span).
12
+ #
13
+ # Confium::Audit.sink = Confium::Audit::OtlpSink.new(
14
+ # endpoint: 'http://localhost:4318/v1/logs',
15
+ # headers: { 'Authorization' => "Bearer #{token}" },
16
+ # service_name: 'confium-issuer'
17
+ # )
18
+ #
19
+ # Delivery is synchronous per record; failures are reported on
20
+ # $stderr and the record is dropped (the audit core's policy — a
21
+ # telemetry outage must never break signing). stdlib only.
22
+ class OtlpSink < Sink
23
+ DEFAULT_ENDPOINT = 'http://localhost:4318/v1/logs'
24
+ SEVERITY = { 'success' => 9, 'failure' => 17, 'error' => 17 }.freeze # INFO / ERROR
25
+
26
+ def initialize(endpoint: DEFAULT_ENDPOINT, headers: {}, service_name: 'confium', timeout: 5)
27
+ super()
28
+ @uri = URI.parse(endpoint)
29
+ @headers = headers
30
+ @service_name = service_name
31
+ @timeout = timeout
32
+ @dropped = 0
33
+ end
34
+
35
+ # Records dropped after failed delivery (diagnostic only).
36
+ attr_reader :dropped
37
+
38
+ def write(record)
39
+ payload = envelope(record)
40
+ response = Net::HTTP.post(@uri, JSON.generate(payload), headers.merge('Content-Type' => 'application/json'))
41
+ raise "collector responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
42
+ rescue StandardError => e
43
+ @dropped += 1
44
+ warn "confium: OTLP delivery failed, audit record dropped (##{@dropped}): #{e.class}: #{e.message}"
45
+ end
46
+
47
+ def close; end
48
+
49
+ private
50
+
51
+ def headers
52
+ { 'User-Agent' => "confium-otlp-sink #{Confium::VERSION}" }.merge(@headers)
53
+ end
54
+
55
+ # OTLP/JSON logs request: one resource, one scope, one record.
56
+ def envelope(record)
57
+ {
58
+ 'resourceLogs' => [
59
+ {
60
+ 'resource' => {
61
+ 'attributes' => [
62
+ { 'key' => 'service.name', 'value' => { 'stringValue' => @service_name } },
63
+ { 'key' => 'telemetry.sdk.name', 'value' => { 'stringValue' => 'confium' } },
64
+ { 'key' => 'telemetry.sdk.version', 'value' => { 'stringValue' => Confium::VERSION } }
65
+ ]
66
+ },
67
+ 'scopeLogs' => [
68
+ {
69
+ 'scope' => { 'name' => 'confium.audit' },
70
+ 'logRecords' => [log_record(record)]
71
+ }
72
+ ]
73
+ }
74
+ ]
75
+ }
76
+ end
77
+
78
+ def log_record(record)
79
+ {
80
+ 'timeUnixNano' => (parse_time(record['timestamp']) * 1_000_000_000).to_i.to_s,
81
+ 'severityNumber' => SEVERITY.fetch(record['result'], 9),
82
+ 'severityText' => record['result'] == 'success' ? 'INFO' : 'ERROR',
83
+ 'body' => { 'stringValue' => "#{record['operation']} #{record['result']}".strip },
84
+ 'attributes' => attributes_for(record)
85
+ }
86
+ end
87
+
88
+ def attributes_for(record)
89
+ record.except('timestamp').map do |key, value|
90
+ { 'key' => key, 'value' => { 'stringValue' => value.to_s } }
91
+ end
92
+ end
93
+
94
+ def parse_time(timestamp)
95
+ return Time.now.to_f unless timestamp.is_a?(String)
96
+
97
+ Time.parse(timestamp).to_f
98
+ rescue ArgumentError
99
+ Time.now.to_f
100
+ end
101
+ end
102
+ end
103
+ end
data/lib/confium/audit.rb CHANGED
@@ -124,3 +124,5 @@ module Confium
124
124
  end
125
125
  end
126
126
  end
127
+
128
+ require_relative 'audit/otlp_sink'
@@ -16,6 +16,32 @@ module Confium
16
16
  class Signature
17
17
  BINARY_FIELDS = %w[public_key signature].freeze
18
18
 
19
+ # Serialize the components this instance was built from, in the
20
+ # canonical wire format. Instances remember their source: built
21
+ # from components, or from a JSON document (which is emitted
22
+ # verbatim when it was canonical).
23
+ def to_json(*_args)
24
+ cached = @confium_components_json
25
+ return cached if cached
26
+
27
+ components = @confium_components or
28
+ raise Confium::Error, 'Signature was not built with component data; use Signature.new or from_json'
29
+ Signature.components_to_json(components)
30
+ end
31
+
32
+ class << self
33
+ # magnus exposes the constructor as a class-level `new`;
34
+ # capture it before this reopen shadows it (plain `super`
35
+ # falls through to Class#new instead).
36
+ alias __native_new new
37
+
38
+ def new(*args)
39
+ sig = __native_new(*args)
40
+ sig.instance_variable_set(:@confium_components, args.first) if args.first.is_a?(Array)
41
+ sig
42
+ end
43
+ end
44
+
19
45
  def self.components_to_json(components)
20
46
  JSON.generate(components.map do |component|
21
47
  component.transform_values do |value|
@@ -35,7 +61,19 @@ module Confium
35
61
  raise ArgumentError, 'expected a non-empty "components" array'
36
62
  end
37
63
 
38
- new(components.map { |c| decode_binary_fields(c) })
64
+ sig = new(components.map { |c| decode_binary_fields(c) })
65
+ sig.instance_variable_set(:@confium_components, components)
66
+ sig.instance_variable_set(:@confium_components_json, canonical_json(json, data))
67
+ sig
68
+ end
69
+
70
+ # The canonical wire form of whatever the caller handed us: a
71
+ # String passes through untouched; parsed structures are
72
+ # re-generated from their components array.
73
+ def self.canonical_json(json, data)
74
+ return json if json.is_a?(String)
75
+
76
+ JSON.generate(data.is_a?(Hash) ? data['components'] : data)
39
77
  end
40
78
 
41
79
  def self.decode_binary_fields(component)
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Confium::TC::Coordinator wraps the async session coordinator service.
4
- #
5
- # The coordinator enables globally distributed threshold signers to
6
- # participate when convenientno simultaneity required.
3
+ # Confium::TC::Coordinator coordinates an in-process threshold
4
+ # signing session: signers submit commitments and shares at their own
5
+ # pace; once the threshold is met, aggregation runs the real
6
+ # threshold-ECDSA combine (CMP20 or GG18 the same in-process
7
+ # drivers behind Confium::TC::Cmp20 / Confium::TC::Gg18) and returns
8
+ # a 64-byte (r, s) signature verifiable under the quorum public key.
7
9
  #
8
10
  # Usage:
9
11
  # coordinator = Confium::TC::Coordinator.new(quorum_id: "biml-root")
10
12
  # session_id = coordinator.create_session(message: data,
11
- # threshold: 5,
12
- # unlock_window: 14400)
13
+ # threshold: 3,
14
+ # scheme: "CMP20-ECDSA-P256")
13
15
  # coordinator.submit_commitment(session_id, signer_id, commitment_bytes)
14
16
  # # ... wait for T commitments ...
15
17
  # coordinator.submit_share(session_id, signer_id, share_bytes)
@@ -25,12 +27,20 @@ module Confium
25
27
  @sessions = {}
26
28
  end
27
29
 
28
- def create_session(message:, threshold:, unlock_window: 14_400)
30
+ SCHEMES = {
31
+ 'CMP20-ECDSA-P256' => ->(shares, threshold, message) { Cmp20.sign(shares, threshold, message) },
32
+ 'GG18-ECDSA-P256' => ->(shares, threshold, message) { Gg18.sign(shares, threshold, message) }
33
+ }.freeze
34
+
35
+ def create_session(message:, threshold:, unlock_window: 14_400, scheme: 'CMP20-ECDSA-P256')
36
+ raise ArgumentError, "unknown scheme: #{scheme} (known: #{SCHEMES.keys.join(', ')})" unless SCHEMES.key?(scheme)
37
+
29
38
  session_id = "session-#{@sessions.length + 1}"
30
39
  @sessions[session_id] = {
31
40
  message: message,
32
41
  threshold: threshold,
33
42
  unlock_window: unlock_window,
43
+ scheme: scheme,
34
44
  state: :pending,
35
45
  commitments: [],
36
46
  shares: []
@@ -57,11 +67,14 @@ module Confium
57
67
 
58
68
  def aggregate(session_id)
59
69
  session = @sessions[session_id] or raise "Unknown session: #{session_id}"
60
- raise 'Threshold not met' if session[:shares].length < session[:threshold]
70
+ raise ThresholdError, 'Threshold not met' if session[:shares].length < session[:threshold]
61
71
 
62
72
  session[:state] = :completed
63
- # In real implementation, calls FFI to aggregate shares
64
- session[:shares].map { |s| s[:bytes] }.join
73
+ SCHEMES.fetch(session[:scheme]).call(
74
+ session[:shares].map { |s| s[:bytes] },
75
+ session[:threshold],
76
+ session[:message]
77
+ )
65
78
  end
66
79
  end
67
80
  end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'socket'
5
+ require_relative 'coordinator'
6
+
7
+ module Confium
8
+ module TC
9
+ # Networked threshold-signing coordinator service.
10
+ #
11
+ # Wraps Confium::TC::Coordinator (real CMP20/GG18 combine) behind
12
+ # a TCP socket so signers on separate machines submit commitments
13
+ # and shares from their own processes:
14
+ #
15
+ # service = Confium::TC::NetworkCoordinator.new(quorum_id: 'root')
16
+ # service.start # binds 127.0.0.1:<ephemeral>
17
+ # client = Confium::TC::SignerClient.new(port: service.port)
18
+ # sid = client.create_session(message: data, threshold: 3)
19
+ # client.submit_share(sid, 'signer-1', share_blob)
20
+ # client.aggregate(sid) # => 64-byte signature
21
+ #
22
+ # Protocol: one JSON object per line (NDJSON); binary fields are
23
+ # hex-encoded. Unknown operations and Coordinator errors come
24
+ # back as {"error": ..., "message": ...} lines.
25
+ #
26
+ # Transport security: none yet — plain TCP intended for loopback
27
+ # or private networks. The upstream noise-transport session
28
+ # protocol replaces this wholesale (see
29
+ # TODO.full/01-multi-host-threshold-signing.md).
30
+ class NetworkCoordinator
31
+ HANDLERS = {
32
+ 'create' => lambda do |coord, req|
33
+ session_id = coord.create_session(
34
+ message: [req.fetch('message_hex')].pack('H*'),
35
+ threshold: req.fetch('threshold'),
36
+ scheme: req['scheme'] || 'CMP20-ECDSA-P256'
37
+ )
38
+ { 'session_id' => session_id }
39
+ end,
40
+ 'commitment' => lambda do |coord, req|
41
+ session_id = req.fetch('session_id')
42
+ coord.submit_commitment(
43
+ session_id, req.fetch('signer_id'),
44
+ [req.fetch('bytes_hex')].pack('H*')
45
+ )
46
+ { 'ok' => true, 'state' => coord.session_state(session_id).to_s }
47
+ end,
48
+ 'share' => lambda do |coord, req|
49
+ coord.submit_share(
50
+ req.fetch('session_id'), req.fetch('signer_id'),
51
+ [req.fetch('bytes_hex')].pack('H*')
52
+ )
53
+ { 'ok' => true }
54
+ end,
55
+ 'aggregate' => lambda do |coord, req|
56
+ signature = coord.aggregate(req.fetch('session_id'))
57
+ { 'signature_hex' => signature.unpack1('H*') }
58
+ end
59
+ }.freeze
60
+
61
+ class RequestError < StandardError; end
62
+
63
+ attr_reader :quorum_id, :port
64
+
65
+ def initialize(quorum_id:, host: '127.0.0.1', port: 0, coordinator: nil)
66
+ @quorum_id = quorum_id
67
+ @host = host
68
+ @port = port
69
+ @coordinator = coordinator || Coordinator.new(quorum_id: quorum_id)
70
+ @server = nil
71
+ @accept_thread = nil
72
+ @mutex = Mutex.new
73
+ end
74
+
75
+ def start
76
+ raise 'already started' if @server
77
+
78
+ # RBS socket lib lacks the (host, port) overload for new
79
+ @server = TCPServer.new(@host.to_s, @port) # steep:ignore
80
+ @port = @server.addr[1]
81
+ @accept_thread = Thread.new { accept_loop }
82
+ self
83
+ end
84
+
85
+ def stop
86
+ return unless @server
87
+
88
+ server = @server
89
+ @server = nil
90
+ server.close
91
+ @accept_thread&.join(5)
92
+ nil
93
+ end
94
+
95
+ def running?
96
+ !@server.nil?
97
+ end
98
+
99
+ private
100
+
101
+ def accept_loop
102
+ loop do
103
+ server = @server
104
+ break unless server
105
+
106
+ begin
107
+ socket = server.accept
108
+ rescue IOError, Errno::EBADF, Errno::EINVAL
109
+ break # listener closed by #stop
110
+ end
111
+ Thread.new(socket) { |conn| serve(conn) }
112
+ end
113
+ end
114
+
115
+ def serve(socket)
116
+ socket.each_line do |line|
117
+ request = JSON.parse(line)
118
+ response = @mutex.synchronize { dispatch(request) }
119
+ socket.write("#{JSON.generate(response)}\n")
120
+ end
121
+ rescue JSON::ParserError => e
122
+ write_error(socket, 'RequestError', "malformed JSON: #{e.message}")
123
+ rescue StandardError => e
124
+ write_error(socket, e.class.name, e.message)
125
+ ensure
126
+ socket.close
127
+ end
128
+
129
+ def dispatch(request)
130
+ op = request['op'] or raise RequestError, 'missing "op"'
131
+ handler = HANDLERS[op] or raise RequestError, "unknown op: #{op}"
132
+
133
+ handler.call(@coordinator, request)
134
+ end
135
+
136
+ def write_error(socket, klass, message)
137
+ socket.write("#{JSON.generate({ 'error' => klass, 'message' => message })}\n")
138
+ rescue IOError
139
+ nil
140
+ end
141
+ end
142
+
143
+ # Client side for NetworkCoordinator. One instance per signer
144
+ # process; each call opens its own connection, so signers never
145
+ # share state.
146
+ class SignerClient
147
+ class RemoteError < StandardError
148
+ attr_reader :remote_class
149
+
150
+ def initialize(remote_class, message)
151
+ @remote_class = remote_class
152
+ super("#{remote_class}: #{message}")
153
+ end
154
+ end
155
+
156
+ def initialize(port:, host: '127.0.0.1')
157
+ @host = host
158
+ @port = port
159
+ end
160
+
161
+ def create_session(message:, threshold:, scheme: 'CMP20-ECDSA-P256')
162
+ response = call(
163
+ 'op' => 'create',
164
+ 'message_hex' => message.to_s.unpack1('H*'),
165
+ 'threshold' => threshold,
166
+ 'scheme' => scheme
167
+ )
168
+ response.fetch('session_id')
169
+ end
170
+
171
+ def submit_commitment(session_id, signer_id, commitment_bytes)
172
+ call(
173
+ 'op' => 'commitment',
174
+ 'session_id' => session_id,
175
+ 'signer_id' => signer_id,
176
+ 'bytes_hex' => commitment_bytes.unpack1('H*')
177
+ ).fetch('ok')
178
+ end
179
+
180
+ def submit_share(session_id, signer_id, share_bytes)
181
+ call(
182
+ 'op' => 'share',
183
+ 'session_id' => session_id,
184
+ 'signer_id' => signer_id,
185
+ 'bytes_hex' => share_bytes.unpack1('H*')
186
+ ).fetch('ok')
187
+ end
188
+
189
+ def aggregate(session_id)
190
+ response = call('op' => 'aggregate', 'session_id' => session_id)
191
+ [response.fetch('signature_hex')].pack('H*')
192
+ end
193
+
194
+ private
195
+
196
+ def call(request)
197
+ TCPSocket.open(@host, @port) do |socket|
198
+ socket.write("#{JSON.generate(request)}\n")
199
+ line = socket.readline
200
+ response = JSON.parse(line)
201
+ raise RemoteError.new(response['error'], response['message']) if response['error']
202
+
203
+ response
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
data/lib/confium/tc.rb CHANGED
@@ -11,7 +11,14 @@
11
11
  # for the full interface specification.
12
12
  module Confium
13
13
  module TC
14
+ # The native extension defines Confium::TC; this namespace file is
15
+ # eager-required from confium.rb, so autoloads registered here do
16
+ # fire. Coordinator and ShareFile are pure Ruby and load with the
17
+ # namespace; Session wraps the engine via FFI and needs the
18
+ # external libconfium dylib, so it stays lazy.
14
19
  autoload :Session, 'confium/tc/session'
15
- autoload :Coordinator, 'confium/tc/coordinator'
20
+ require_relative 'tc/coordinator'
21
+ require_relative 'tc/network_coordinator'
22
+ require_relative 'tc/share_file'
16
23
  end
17
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Confium
4
- VERSION = '0.4.1'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/confium.rb CHANGED
@@ -20,7 +20,11 @@ begin
20
20
  # directory.
21
21
  minor = RUBY_VERSION[/\A\d+\.\d+/]
22
22
  dlext = RbConfig::CONFIG['DLEXT'] || 'so'
23
- candidates = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') ? [minor, '3.3'].uniq : [minor]
23
+ major = RUBY_VERSION[/\A\d+/].to_i
24
+ # 3.3-window binaries load on 3.3/3.4 only; a cross-major load
25
+ # (4.x) fails TypedData class checks, so the fallback applies
26
+ # within the 3.x line alone.
27
+ candidates = major == 3 && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') ? [minor, '3.3'].uniq : [minor]
24
28
  # Windows gems carry an exact-minor window per Ruby (a PE import
25
29
  # names the version-specific ruby DLL); other platforms share the
26
30
  # 3.3 window for 3.3+. Prefer the exact minor when present.
@@ -92,7 +96,8 @@ require_relative 'confium/transparency'
92
96
  # of that module.
93
97
  require_relative 'confium/audit'
94
98
 
95
- # Eager-load the TC ShareFile Ruby companion. The native extension
96
- # defines `Confium::TC` as a Ruby module; this file adds the
97
- # `ShareFile` class for filesystem-backed share persistence.
98
- require_relative 'confium/tc/share_file'
99
+ # Eager-load the TC namespace file: `Confium::TC` is native-defined,
100
+ # so an autoload here would never fire (the PKI pattern). It
101
+ # registers the pure-Ruby Session/Coordinator companions and the
102
+ # ShareFile class for filesystem-backed share persistence.
103
+ require_relative 'confium/tc'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: confium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Open
@@ -128,6 +128,7 @@ files:
128
128
  - ext/confium_native/src/util.rs
129
129
  - lib/confium.rb
130
130
  - lib/confium/audit.rb
131
+ - lib/confium/audit/otlp_sink.rb
131
132
  - lib/confium/cfm.rb
132
133
  - lib/confium/composite.rb
133
134
  - lib/confium/crypto.rb
@@ -155,6 +156,7 @@ files:
155
156
  - lib/confium/secure_bytes.rb
156
157
  - lib/confium/tc.rb
157
158
  - lib/confium/tc/coordinator.rb
159
+ - lib/confium/tc/network_coordinator.rb
158
160
  - lib/confium/tc/session.rb
159
161
  - lib/confium/tc/session_stub.rb
160
162
  - lib/confium/tc/share_file.rb