confium 0.4.1-aarch64-linux → 0.6.0-aarch64-linux

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.
@@ -0,0 +1,120 @@
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, retried with exponential
20
+ # backoff (retries: attempts after the first, base doubled per
21
+ # attempt). After the final failure the record is dropped with a
22
+ # $stderr report — the audit core's policy: a telemetry outage
23
+ # must never break signing. stdlib only.
24
+ class OtlpSink < Sink
25
+ DEFAULT_ENDPOINT = 'http://localhost:4318/v1/logs'
26
+ SEVERITY = { 'success' => 9, 'failure' => 17, 'error' => 17 }.freeze # INFO / ERROR
27
+
28
+ def initialize(endpoint: DEFAULT_ENDPOINT, headers: {}, service_name: 'confium', timeout: 5,
29
+ retries: 2, retry_base: 0.1)
30
+ super()
31
+ @uri = URI.parse(endpoint)
32
+ @headers = headers
33
+ @service_name = service_name
34
+ @timeout = timeout
35
+ @retries = retries
36
+ @retry_base = retry_base
37
+ @dropped = 0
38
+ end
39
+
40
+ # Records dropped after failed delivery (diagnostic only).
41
+ attr_reader :dropped
42
+
43
+ def write(record)
44
+ payload = envelope(record)
45
+ deliver(JSON.generate(payload))
46
+ rescue StandardError => e
47
+ @dropped += 1
48
+ warn "confium: OTLP delivery failed, audit record dropped (##{@dropped}): #{e.class}: #{e.message}"
49
+ end
50
+
51
+ def close; end
52
+
53
+ private
54
+
55
+ # Attempts grow the backoff: retry_base * 2**attempt. When
56
+ # retry_base is 0 (specs) retries run without delay.
57
+ def deliver(body)
58
+ (@retries + 1).times do |attempt|
59
+ sleep(@retry_base * (2**attempt)) if attempt.positive? && @retry_base.positive?
60
+
61
+ response = Net::HTTP.post(@uri, body, headers.merge('Content-Type' => 'application/json'))
62
+ return response if response.is_a?(Net::HTTPSuccess)
63
+
64
+ raise "collector responded #{response.code}" if attempt == @retries
65
+ end
66
+ end
67
+
68
+ def headers
69
+ { 'User-Agent' => "confium-otlp-sink #{Confium::VERSION}" }.merge(@headers)
70
+ end
71
+
72
+ # OTLP/JSON logs request: one resource, one scope, one record.
73
+ def envelope(record)
74
+ {
75
+ 'resourceLogs' => [
76
+ {
77
+ 'resource' => {
78
+ 'attributes' => [
79
+ { 'key' => 'service.name', 'value' => { 'stringValue' => @service_name } },
80
+ { 'key' => 'telemetry.sdk.name', 'value' => { 'stringValue' => 'confium' } },
81
+ { 'key' => 'telemetry.sdk.version', 'value' => { 'stringValue' => Confium::VERSION } }
82
+ ]
83
+ },
84
+ 'scopeLogs' => [
85
+ {
86
+ 'scope' => { 'name' => 'confium.audit' },
87
+ 'logRecords' => [log_record(record)]
88
+ }
89
+ ]
90
+ }
91
+ ]
92
+ }
93
+ end
94
+
95
+ def log_record(record)
96
+ {
97
+ 'timeUnixNano' => (parse_time(record['timestamp']) * 1_000_000_000).to_i.to_s,
98
+ 'severityNumber' => SEVERITY.fetch(record['result'], 9),
99
+ 'severityText' => record['result'] == 'success' ? 'INFO' : 'ERROR',
100
+ 'body' => { 'stringValue' => "#{record['operation']} #{record['result']}".strip },
101
+ 'attributes' => attributes_for(record)
102
+ }
103
+ end
104
+
105
+ def attributes_for(record)
106
+ record.except('timestamp').map do |key, value|
107
+ { 'key' => key, 'value' => { 'stringValue' => value.to_s } }
108
+ end
109
+ end
110
+
111
+ def parse_time(timestamp)
112
+ return Time.now.to_f unless timestamp.is_a?(String)
113
+
114
+ Time.parse(timestamp).to_f
115
+ rescue ArgumentError
116
+ Time.now.to_f
117
+ end
118
+ end
119
+ end
120
+ 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)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Confium
4
+ # Pure resolution of which native-extension ABI windows to try, in
5
+ # order. Extracted from the loader so the decision — the piece with
6
+ # the release-incident history (3.1-window segfaults on 3.2, the
7
+ # cross-major 3.3→4.0 TypeError, windowless platform gems) — is
8
+ # testable without installing eight platform gems.
9
+ module NativeWindows
10
+ # Ordered candidate windows for +ruby_version+ (e.g. "3.4.8").
11
+ #
12
+ # Background: platform gems ship one extension per ABI window.
13
+ # Ruby 3.2 broke the 3.1 ABI (object shapes) and rb-sys references
14
+ # a VM pointer libruby stopped exporting in 3.3, so 3.1/3.2/4.0
15
+ # get exact-minor builds while a 3.3-window binary loads on both
16
+ # 3.3 and 3.4. The 3.3 fallback applies within the 3.x line
17
+ # alone: a cross-major load (4.x) fails TypedData class-identity
18
+ # checks, so 4.0 must not fall back to it.
19
+ def self.candidates(ruby_version)
20
+ minor = ruby_version[/\A\d+\.\d+/]
21
+ raise ArgumentError, "not a ruby version: #{ruby_version.inspect}" unless minor
22
+
23
+ major = ruby_version[/\A\d+/].to_i
24
+ if major == 3 && Gem::Version.new(ruby_version) >= Gem::Version.new('3.3')
25
+ [minor, '3.3'].uniq
26
+ else
27
+ [minor]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,15 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Confium::TC::Coordinator wraps the async session coordinator service.
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) and returns a 64-byte
7
+ # (r, s) signature verifiable under the quorum public key.
4
8
  #
5
- # The coordinator enables globally distributed threshold signers to
6
- # participate when convenient — no simultaneity required.
9
+ # The session semantics (state machine, duplicate-signer rejection,
10
+ # the combine) live in SigningSession; this class is the session
11
+ # registry and quorum-scoped entry point. NetworkCoordinator is the
12
+ # TCP/NDJSON adapter over the same sessions.
7
13
  #
8
14
  # Usage:
9
15
  # coordinator = Confium::TC::Coordinator.new(quorum_id: "biml-root")
10
16
  # session_id = coordinator.create_session(message: data,
11
- # threshold: 5,
12
- # unlock_window: 14400)
17
+ # threshold: 3,
18
+ # scheme: "CMP20-ECDSA-P256")
13
19
  # coordinator.submit_commitment(session_id, signer_id, commitment_bytes)
14
20
  # # ... wait for T commitments ...
15
21
  # coordinator.submit_share(session_id, signer_id, share_bytes)
@@ -25,43 +31,38 @@ module Confium
25
31
  @sessions = {}
26
32
  end
27
33
 
28
- def create_session(message:, threshold:, unlock_window: 14_400)
34
+ def create_session(message:, threshold:, unlock_window: SigningSession::DEFAULT_UNLOCK_WINDOW,
35
+ scheme: 'CMP20-ECDSA-P256')
29
36
  session_id = "session-#{@sessions.length + 1}"
30
- @sessions[session_id] = {
31
- message: message,
32
- threshold: threshold,
33
- unlock_window: unlock_window,
34
- state: :pending,
35
- commitments: [],
36
- shares: []
37
- }
37
+ @sessions[session_id] = SigningSession.new(
38
+ message: message, threshold: threshold,
39
+ unlock_window: unlock_window, scheme: scheme
40
+ )
38
41
  session_id
39
42
  end
40
43
 
41
44
  def session_state(session_id)
42
- @sessions.dig(session_id, :state)
45
+ fetch(session_id).state
43
46
  end
44
47
 
45
48
  def submit_commitment(session_id, signer_id, commitment_bytes)
46
- session = @sessions[session_id] or raise "Unknown session: #{session_id}"
47
- session[:commitments] << { signer_id: signer_id, bytes: commitment_bytes }
48
- return unless session[:commitments].length >= session[:threshold]
49
-
50
- session[:state] = :commitments_collected
49
+ fetch(session_id).add_commitment(signer_id, commitment_bytes)
51
50
  end
52
51
 
53
52
  def submit_share(session_id, signer_id, share_bytes)
54
- session = @sessions[session_id] or raise "Unknown session: #{session_id}"
55
- session[:shares] << { signer_id: signer_id, bytes: share_bytes }
53
+ fetch(session_id).add_share(signer_id, share_bytes)
56
54
  end
57
55
 
58
56
  def aggregate(session_id)
59
- session = @sessions[session_id] or raise "Unknown session: #{session_id}"
60
- raise 'Threshold not met' if session[:shares].length < session[:threshold]
57
+ fetch(session_id).aggregate
58
+ end
59
+
60
+ private
61
61
 
62
- session[:state] = :completed
63
- # In real implementation, calls FFI to aggregate shares
64
- session[:shares].map { |s| s[:bytes] }.join
62
+ def fetch(session_id)
63
+ @sessions.fetch(session_id) do
64
+ raise NotFoundError.new("Unknown session: #{session_id}", kind: :session, identifier: session_id)
65
+ end
65
66
  end
66
67
  end
67
68
  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
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Confium
4
+ module TC
5
+ # One threshold-signing session: the state machine and the combine
6
+ # behind every coordinator surface. Coordinator (in-process) and
7
+ # NetworkCoordinator (TCP/NDJSON) are adapters over this class —
8
+ # they own registries and framing; the session owns the semantics:
9
+ #
10
+ # pending → commitments_collected (T distinct commitments)
11
+ # → completed (aggregate runs the scheme combine)
12
+ #
13
+ # Aggregate may be called again after completion — the combine
14
+ # randomizes its nonce, so each call yields a fresh (verifiable)
15
+ # signature over the same shares, which is what a network client
16
+ # retrying after a dropped response needs.
17
+ #
18
+ # Signer identity is load-bearing: one signer submitting two
19
+ # shares must never count twice toward the threshold, so
20
+ # commitments and shares are keyed by signer_id and duplicates
21
+ # raise.
22
+ class SigningSession
23
+ DEFAULT_UNLOCK_WINDOW = 14_400
24
+
25
+ SCHEMES = {
26
+ 'CMP20-ECDSA-P256' => ->(shares, threshold, message) { Cmp20.sign(shares, threshold, message) },
27
+ 'GG18-ECDSA-P256' => ->(shares, threshold, message) { Gg18.sign(shares, threshold, message) }
28
+ }.freeze
29
+
30
+ attr_reader :message, :threshold, :unlock_window, :scheme, :state
31
+
32
+ def initialize(message:, threshold:, unlock_window: DEFAULT_UNLOCK_WINDOW, scheme: 'CMP20-ECDSA-P256')
33
+ raise ArgumentError, "unknown scheme: #{scheme} (known: #{SCHEMES.keys.join(', ')})" unless SCHEMES.key?(scheme)
34
+
35
+ @message = message
36
+ @threshold = threshold
37
+ @unlock_window = unlock_window
38
+ @scheme = scheme
39
+ @state = :pending
40
+ # @type ivar @commitments: Hash[String, String]
41
+ @commitments = {}
42
+ # @type ivar @shares: Hash[String, String]
43
+ @shares = {}
44
+ end
45
+
46
+ def add_commitment(signer_id, commitment_bytes)
47
+ record(@commitments, signer_id, commitment_bytes)
48
+ @state = :commitments_collected if @commitments.length >= @threshold
49
+ self
50
+ end
51
+
52
+ def add_share(signer_id, share_bytes)
53
+ record(@shares, signer_id, share_bytes)
54
+ self
55
+ end
56
+
57
+ def commitment_count
58
+ @commitments.length
59
+ end
60
+
61
+ def share_count
62
+ @shares.length
63
+ end
64
+
65
+ def threshold_met?
66
+ @shares.length >= @threshold
67
+ end
68
+
69
+ def aggregate
70
+ unless threshold_met?
71
+ raise ThresholdError.new('Threshold not met', have_count: @shares.length, need_count: @threshold)
72
+ end
73
+
74
+ signature = SCHEMES.fetch(@scheme).call(@shares.values, @threshold, @message)
75
+ @state = :completed
76
+ signature
77
+ end
78
+
79
+ private
80
+
81
+ def record(store, signer_id, bytes)
82
+ if store.key?(signer_id)
83
+ raise ValidationError.new(
84
+ "duplicate submission from signer #{signer_id}",
85
+ param: :signer_id, expected: 'one submission per signer', actual: signer_id
86
+ )
87
+ end
88
+
89
+ store[signer_id] = bytes
90
+ end
91
+ end
92
+ end
93
+ end
data/lib/confium/tc.rb CHANGED
@@ -11,7 +11,12 @@
11
11
  # for the full interface specification.
12
12
  module Confium
13
13
  module TC
14
- autoload :Session, 'confium/tc/session'
15
- autoload :Coordinator, 'confium/tc/coordinator'
14
+ # The native extension defines Confium::TC; this namespace file
15
+ # is eager-required from confium.rb, so autoloads never fire —
16
+ # the pure-Ruby companions load with the namespace directly.
17
+ require_relative 'tc/signing_session'
18
+ require_relative 'tc/coordinator'
19
+ require_relative 'tc/network_coordinator'
20
+ require_relative 'tc/share_file'
16
21
  end
17
22
  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.6.0'
5
5
  end
data/lib/confium.rb CHANGED
@@ -10,21 +10,14 @@
10
10
  # are loaded lazily via autoload — see lib/confium/<name>.rb.
11
11
 
12
12
  require_relative 'confium/version'
13
+ require_relative 'confium/native_windows'
13
14
 
14
15
  begin
15
- # Pre-built platform gems ship one extension per C-ABI window
16
- # (Ruby 3.2 broke the 3.1 ABI — object shapes — and rb-sys
17
- # references the VM pointer libruby stopped exporting in 3.3, so
18
- # 3.1 and 3.2 get exact-minor builds while 3.3 covers 3.3+).
19
- # Source builds install the extension flat, without a version
20
- # directory.
21
- minor = RUBY_VERSION[/\A\d+\.\d+/]
16
+ # Pre-built platform gems ship one extension per ABI window (the
17
+ # window rules live in Confium::NativeWindows). Source builds
18
+ # install the extension flat, without a version directory.
22
19
  dlext = RbConfig::CONFIG['DLEXT'] || 'so'
23
- candidates = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') ? [minor, '3.3'].uniq : [minor]
24
- # Windows gems carry an exact-minor window per Ruby (a PE import
25
- # names the version-specific ruby DLL); other platforms share the
26
- # 3.3 window for 3.3+. Prefer the exact minor when present.
27
- windowed = candidates.filter_map do |w|
20
+ windowed = Confium::NativeWindows.candidates(RUBY_VERSION).filter_map do |w|
28
21
  path = File.expand_path("confium_native/#{w}/confium_native.#{dlext}", __dir__ || '.')
29
22
  w if File.exist?(path)
30
23
  end
@@ -92,7 +85,8 @@ require_relative 'confium/transparency'
92
85
  # of that module.
93
86
  require_relative 'confium/audit'
94
87
 
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'
88
+ # Eager-load the TC namespace file: `Confium::TC` is native-defined,
89
+ # so an autoload here would never fire (the PKI pattern). It
90
+ # registers the pure-Ruby Session/Coordinator companions and the
91
+ # ShareFile class for filesystem-backed share persistence.
92
+ require_relative 'confium/tc'