ree 1.2.4 → 1.2.7

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: a73f9d4ffa414ae499d87732f896c437117f65b52b7675908eb8581d482946f4
4
- data.tar.gz: '0328c44c6cb7d7dddae52494fb057e1b88f84a8d2c7f28ea62c202bcb1c74125'
3
+ metadata.gz: c6f558ae43347082febc5e6f30be8afe0ec5340423b9e2cca23d9b412139ab4e
4
+ data.tar.gz: 5a1dddb63ed9e12178ff17c3150eb8910fb29b63c564d78302dddf77dcc679a0
5
5
  SHA512:
6
- metadata.gz: 367764b547db08f4c975a0ea141c7563e5536612d05bf48388cf8b9100dc381573dcdb9081b6c73a6880ff9bc57fbe45cbda09c51030aa370bd032997a372cd2
7
- data.tar.gz: 4177a967cc3fa694fc03b6302fc10de386dea9ec2c328d16fbe53f4a765c1b4e0d25e9817d99ea0100fca3975b470a73192bc848d77684235771524651e06657
6
+ metadata.gz: f0f8936853687e6facfd21ea46bea355f593b6cf570ac20c6c3406e1764463432623664c829d207bc2e1ccd241baf4e99efc0e409518e596cfcbf5a20af4488a
7
+ data.tar.gz: 5950203f5babaf701bd5a9c48e9e51fabda3844f9e7645f0912fcd98f1cb109c488211c39119f6af1bcb918c8c34c2e74c8d55e933cc8aabe046dd94c0013fdd
data/Gemfile.lock CHANGED
@@ -1,13 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ree (1.2.4)
4
+ ree (1.2.7)
5
+ base64
5
6
  commander (~> 5.0.0)
6
7
  logger (~> 1.6.5)
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
10
11
  specs:
12
+ base64 (0.3.0)
11
13
  commander (5.0.0)
12
14
  highline (~> 3.0.0)
13
15
  debug (1.9.1)
data/exe/ree CHANGED
@@ -223,6 +223,87 @@ class ReeCliRunner
223
223
  end
224
224
  end
225
225
 
226
+ command :"licensing.register_client" do |c|
227
+ c.syntax = 'ree licensing.register_client --name=NAME --contact=CONTACT'
228
+ c.description = 'register a new licensing client'
229
+ c.summary = '> ' + c.description
230
+ c.example 'register a client', 'ree licensing.register_client --name="Glabix" --contact="hello@glabix.com" --metadata="type=enterprise,country=RU"'
231
+ c.option '--name NAME', String, 'Client name'
232
+ c.option '--contact CONTACT', String, 'Client contact email'
233
+ c.option '--metadata METADATA', String, 'Client metadata (key=value pairs separated by commas)'
234
+ c.option '--clients_dir DIR', String, 'Directory for clients.json (default: current dir)'
235
+ c.action do |args, options|
236
+ options_hash = options.__hash__
237
+ options_hash.delete(:trace)
238
+
239
+ raise Ree::Error.new("--name is required") unless options_hash[:name]
240
+ raise Ree::Error.new("--contact is required") unless options_hash[:contact]
241
+
242
+ Ree::CLI::Licensing::RegisterClient.run(
243
+ name: options_hash[:name],
244
+ contact: options_hash[:contact],
245
+ metadata_str: options_hash[:metadata],
246
+ clients_dir: options_hash[:clients_dir] || Dir.pwd
247
+ )
248
+ end
249
+ end
250
+
251
+ command :"licensing.obfuscate" do |c|
252
+ c.syntax = 'ree licensing.obfuscate --source_path=PATH --target_path=PATH --client_id=ID --expires_at=DATE'
253
+ c.description = 'obfuscate a Ree project for distribution'
254
+ c.summary = '> ' + c.description
255
+ c.example 'obfuscate project', 'ree licensing.obfuscate --source_path=./project --target_path=./dist --client_id=client_abc123 --expires_at="2026-12-31"'
256
+ c.option '--source_path PATH', String, 'Source project path'
257
+ c.option '--target_path PATH', String, 'Target distribution path'
258
+ c.option '--client_id ID', String, 'Client ID from clients.json'
259
+ c.option '--expires_at DATE', String, 'License expiration date (YYYY-MM-DD)'
260
+ c.option '--exclude_files FILES', String, 'Comma-separated list of files to exclude'
261
+ c.option '--clients_dir DIR', String, 'Directory for clients.json (default: current dir)'
262
+ c.action do |args, options|
263
+ options_hash = options.__hash__
264
+ options_hash.delete(:trace)
265
+
266
+ raise Ree::Error.new("--source_path is required") unless options_hash[:source_path]
267
+ raise Ree::Error.new("--target_path is required") unless options_hash[:target_path]
268
+ raise Ree::Error.new("--client_id is required") unless options_hash[:client_id]
269
+ raise Ree::Error.new("--expires_at is required") unless options_hash[:expires_at]
270
+
271
+ Ree::CLI::Licensing::Obfuscate.run(
272
+ source_path: options_hash[:source_path],
273
+ target_path: options_hash[:target_path],
274
+ client_id: options_hash[:client_id],
275
+ expires_at: options_hash[:expires_at],
276
+ exclude_files: options_hash[:exclude_files] || "",
277
+ clients_dir: options_hash[:clients_dir] || Dir.pwd
278
+ )
279
+ end
280
+ end
281
+
282
+ command :"licensing.renew_license" do |c|
283
+ c.syntax = 'ree licensing.renew_license --client_id=ID --expires_at=DATE'
284
+ c.description = 'renew a license for an existing client'
285
+ c.summary = '> ' + c.description
286
+ c.example 'renew license', 'ree licensing.renew_license --client_id=client_abc123 --expires_at="2028-01-01"'
287
+ c.option '--client_id ID', String, 'Client ID from clients.json'
288
+ c.option '--expires_at DATE', String, 'New license expiration date (YYYY-MM-DD)'
289
+ c.option '--output_path PATH', String, 'Output path for license.json'
290
+ c.option '--clients_dir DIR', String, 'Directory for clients.json (default: current dir)'
291
+ c.action do |args, options|
292
+ options_hash = options.__hash__
293
+ options_hash.delete(:trace)
294
+
295
+ raise Ree::Error.new("--client_id is required") unless options_hash[:client_id]
296
+ raise Ree::Error.new("--expires_at is required") unless options_hash[:expires_at]
297
+
298
+ Ree::CLI::Licensing::RenewLicense.run(
299
+ client_id: options_hash[:client_id],
300
+ expires_at: options_hash[:expires_at],
301
+ output_path: options_hash[:output_path],
302
+ clients_dir: options_hash[:clients_dir] || Dir.pwd
303
+ )
304
+ end
305
+ end
306
+
226
307
  command :"gen.index_project" do |c|
227
308
  c.syntax = 'ree gen.index_project'
228
309
  c.description = 'generate index schema of methods'
@@ -12,66 +12,48 @@ class Ree::BenchmarkMethodPlugin
12
12
  end
13
13
 
14
14
  def call
15
- return unless @method_name == :call && ree_fn?
15
+ return nil unless @method_name == :call && ree_fn?
16
16
 
17
17
  config = @target.instance_variable_get(:@__ree_benchmark_config)
18
-
18
+ benchmark_name = build_benchmark_name
19
+
19
20
  if config
20
- wrap_as_entry_point(config)
21
+ build_entry_point_wrapper(benchmark_name, config)
21
22
  else
22
- wrap_as_collector
23
+ build_collector_wrapper(benchmark_name)
23
24
  end
24
25
  end
25
26
 
26
27
  private
27
28
 
28
- def wrap_as_entry_point(config)
29
- alias_target = @is_class_method ? eigenclass : @target
30
- method_name = @method_name
31
- method_alias = :"__benchmark_#{method_name}"
32
-
33
- return if alias_target.method_defined?(method_alias)
34
-
35
- alias_target.alias_method(method_alias, method_name)
36
-
37
- benchmark_name = build_benchmark_name
38
- output_proc = config[:output] || -> (res) { $stdout.puts(res) }
29
+ def build_entry_point_wrapper(benchmark_name, config)
30
+ output_proc = config[:output] || ->(res) { $stdout.puts(res) }
39
31
  deep = config.fetch(:deep, true)
40
32
  once = config.fetch(:once, false)
41
33
  benchmark_done = false
42
34
 
43
- alias_target.define_method(method_name) do |*args, **kwargs, &block|
35
+ Proc.new do |instance, next_layer, *args, **kwargs, &block|
44
36
  if Ree::BenchmarkTracer.active?
45
37
  Ree::BenchmarkTracer.collect(benchmark_name) do
46
- send(method_alias, *args, **kwargs, &block)
38
+ next_layer.call(*args, **kwargs, &block)
47
39
  end
48
40
  elsif once && benchmark_done
49
41
  Ree::BenchmarkTracer.collect(benchmark_name) do
50
- send(method_alias, *args, **kwargs, &block)
42
+ next_layer.call(*args, **kwargs, &block)
51
43
  end
52
44
  else
53
45
  benchmark_done = true if once
54
46
  Ree::BenchmarkTracer.trace(benchmark_name, output_proc: output_proc, deep: deep) do
55
- send(method_alias, *args, **kwargs, &block)
47
+ next_layer.call(*args, **kwargs, &block)
56
48
  end
57
49
  end
58
50
  end
59
51
  end
60
52
 
61
- def wrap_as_collector
62
- alias_target = @is_class_method ? eigenclass : @target
63
- method_name = @method_name
64
- method_alias = :"__benchmark_#{method_name}"
65
-
66
- return if alias_target.method_defined?(method_alias)
67
-
68
- alias_target.alias_method(method_alias, method_name)
69
-
70
- benchmark_name = build_benchmark_name
71
-
72
- alias_target.define_method(method_name) do |*args, **kwargs, &block|
53
+ def build_collector_wrapper(benchmark_name)
54
+ Proc.new do |instance, next_layer, *args, **kwargs, &block|
73
55
  Ree::BenchmarkTracer.collect(benchmark_name) do
74
- send(method_alias, *args, **kwargs, &block)
56
+ next_layer.call(*args, **kwargs, &block)
75
57
  end
76
58
  end
77
59
  end
@@ -99,8 +81,4 @@ class Ree::BenchmarkMethodPlugin
99
81
 
100
82
  facade.get_object(pkg, obj).fn?
101
83
  end
102
-
103
- def eigenclass
104
- class << @target; self; end
105
- end
106
84
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ree
4
+ module CLI
5
+ module Licensing
6
+ class Obfuscate
7
+ class << self
8
+ def run(source_path:, target_path:, client_id:, expires_at:, exclude_files: "", clients_dir: Dir.pwd, stdout: $stdout)
9
+ exclude_list = exclude_files.to_s.split(',').map(&:strip).reject(&:empty?)
10
+
11
+ Ree::Licensing::Obfuscator.run(
12
+ source_path: source_path,
13
+ target_path: target_path,
14
+ client_id: client_id,
15
+ expires_at: expires_at,
16
+ clients_dir: clients_dir,
17
+ exclude_files: exclude_list,
18
+ stdout: stdout
19
+ )
20
+ rescue Ree::Error => e
21
+ stdout.puts "Error: #{e.message}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ree
4
+ module CLI
5
+ module Licensing
6
+ class RegisterClient
7
+ class << self
8
+ def run(name:, contact:, metadata_str: nil, clients_dir: Dir.pwd, stdout: $stdout)
9
+ metadata = parse_metadata(metadata_str)
10
+
11
+ store = Ree::Licensing::ClientStore.new(clients_dir)
12
+ client = store.register_client(
13
+ name: name,
14
+ contact: contact,
15
+ metadata: metadata
16
+ )
17
+
18
+ stdout.puts "Client registered successfully:"
19
+ stdout.puts " Client ID: #{client['client_id']}"
20
+ stdout.puts " Name: #{client['name']}"
21
+ stdout.puts " Contact: #{client['contact']}"
22
+ stdout.puts " Saved to: #{File.join(clients_dir, 'clients.json')}"
23
+
24
+ client
25
+ rescue Ree::Error => e
26
+ stdout.puts "Error: #{e.message}"
27
+ end
28
+
29
+ private
30
+
31
+ def parse_metadata(metadata_str)
32
+ return {} if metadata_str.nil? || metadata_str.empty?
33
+
34
+ metadata_str.split(',').each_with_object({}) do |pair, hash|
35
+ key, value = pair.split('=', 2)
36
+ hash[key.strip] = value&.strip
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Ree
6
+ module CLI
7
+ module Licensing
8
+ class RenewLicense
9
+ class << self
10
+ def run(client_id:, expires_at:, output_path: nil, clients_dir: Dir.pwd, stdout: $stdout)
11
+ store = Ree::Licensing::ClientStore.new(clients_dir)
12
+ client = store.find_client(client_id)
13
+ raise Ree::Error.new("Client #{client_id} not found") unless client
14
+
15
+ last_license = store.last_license(client_id)
16
+ raise Ree::Error.new("No existing license found for #{client_id}") unless last_license
17
+
18
+ result = Ree::Licensing::LicenseGenerator.generate(
19
+ client_id: client_id,
20
+ private_key_pem: client['private_key_pem'],
21
+ public_key_pem: client['public_key_pem'],
22
+ aes_key_hex: last_license['aes_key_hex'],
23
+ iv_hex: last_license['iv_hex'],
24
+ expires_at: expires_at
25
+ )
26
+
27
+ store.add_license(client_id, result[:license_record])
28
+
29
+ output = output_path || File.join(clients_dir, "license_#{client_id}.json")
30
+ File.write(output, JSON.pretty_generate(result[:license_file]))
31
+
32
+ stdout.puts "License renewed successfully:"
33
+ stdout.puts " Client ID: #{client_id}"
34
+ stdout.puts " Expires at: #{expires_at}"
35
+ stdout.puts " License file: #{output}"
36
+
37
+ result
38
+ rescue Ree::Error => e
39
+ stdout.puts "Error: #{e.message}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/ree/cli.rb CHANGED
@@ -8,5 +8,11 @@ module Ree
8
8
  autoload :GenerateTemplate, 'ree/cli/generate_template'
9
9
  autoload :Indexing, 'ree/cli/indexing'
10
10
  autoload :SpecRunner, 'ree/cli/spec_runner'
11
+
12
+ module Licensing
13
+ autoload :RegisterClient, 'ree/cli/licensing/register_client'
14
+ autoload :Obfuscate, 'ree/cli/licensing/obfuscate'
15
+ autoload :RenewLicense, 'ree/cli/licensing/renew_license'
16
+ end
11
17
  end
12
18
  end
@@ -7,14 +7,14 @@ require_relative 'engine_proxy'
7
7
  module Ree::Contracts
8
8
  module Contractable
9
9
  def method_added(name)
10
- return if _ree_method_added_hook_active?
11
- MethodDecorator.new(name, false, self).call
10
+ return super if _ree_method_added_hook_active?
11
+ MethodDecorator.new(name, false, self).call(plugin_mode: false)
12
12
  super
13
13
  end
14
14
 
15
15
  def singleton_method_added(name)
16
- return if _ree_method_added_hook_active?
17
- MethodDecorator.new(name, true, self).call
16
+ return super if _ree_method_added_hook_active?
17
+ MethodDecorator.new(name, true, self).call(plugin_mode: false)
18
18
  super
19
19
  end
20
20
 
@@ -51,21 +51,18 @@ module Ree::Contracts
51
51
  @doc = engine.fetch_doc
52
52
  end
53
53
 
54
- def call
55
- return if Ree::Contracts.no_contracts?
56
- return unless contract_definition
54
+ def call(plugin_mode: true)
55
+ return nil if Ree::Contracts.no_contracts?
56
+ return nil unless contract_definition
57
57
 
58
+ # Store decorator for runtime lookups (still needed)
58
59
  self.class.add_decorator(self)
59
60
 
60
- original_alias = :"__ree_original_#{method_name}"
61
- param_source = if alias_target.method_defined?(original_alias)
62
- original_alias
63
- else
64
- method_name
65
- end
66
-
61
+ # Get method parameters from the method (before aliasing)
62
+ # Note: We get params from the current method, not __ree_original_#{method_name}
63
+ # because the alias hasn't been created yet when plugins are called
67
64
  @method_parameters = alias_target
68
- .instance_method(param_source)
65
+ .instance_method(method_name)
69
66
  .parameters
70
67
  .freeze
71
68
 
@@ -77,21 +74,13 @@ module Ree::Contracts
77
74
 
78
75
  @return_validator = Validators.fetch_for(contract_definition.return_contract)
79
76
 
80
- make_alias
81
- make_definition
82
- end
83
-
84
- def execute_on(target, args, kwargs, &blk)
85
- @args.call(args, kwargs, blk)
86
- result = target.send(method_alias, *args, **kwargs, &blk)
87
-
88
- if !return_validator.call(result)
89
- raise ReturnContractError, "Invalid return value for #{printed_name}\n #{
90
- return_validator.message(result, 'returns', 0).strip
91
- }"
77
+ if plugin_mode
78
+ # Plugin mode: Return wrapper lambda for composition
79
+ build_contract_wrapper
80
+ else
81
+ # Legacy mode: Apply wrapper directly (for Contractable standalone usage)
82
+ apply_contract_wrapper_directly
92
83
  end
93
-
94
- result
95
84
  end
96
85
 
97
86
  # Unique ID of this Method Decorator
@@ -99,11 +88,6 @@ module Ree::Contracts
99
88
  @id ||= self.class.decorator_id(target, method_name, is_class_method)
100
89
  end
101
90
 
102
- # Alias name for original method
103
- def method_alias
104
- @method_alias ||= :"__original_#{method_name}_#{SecureRandom.hex}"
105
- end
106
-
107
91
  # Target class to be used for alias method definition
108
92
  def alias_target
109
93
  @alias_target ||= begin
@@ -112,54 +96,90 @@ module Ree::Contracts
112
96
  end
113
97
  end
114
98
 
99
+ # Public method used by legacy contract wrapper (called from class_eval'd method)
100
+ def validate_and_call(instance, method_alias, args, kwargs, &blk)
101
+ @args.call(args, kwargs, blk)
102
+ result = instance.send(method_alias, *args, **kwargs, &blk)
103
+
104
+ unless @return_validator.call(result)
105
+ raise ReturnContractError, "Invalid return value for #{printed_name}\n #{
106
+ @return_validator.message(result, 'returns', 0).strip
107
+ }"
108
+ end
109
+
110
+ result
111
+ end
112
+
115
113
  private
116
114
 
117
- def make_alias
118
- alias_target.alias_method(method_alias, method_name)
115
+ def build_contract_wrapper
116
+ args_validator = @args
117
+ return_validator = @return_validator
118
+ decorator_printed_name = printed_name
119
+
120
+ Proc.new do |instance, next_layer, *args, **kwargs, &block|
121
+ # Validate arguments
122
+ args_validator.call(args, kwargs, block)
123
+
124
+ # Call next layer
125
+ result = next_layer.call(*args, **kwargs, &block)
126
+
127
+ # Validate return value
128
+ unless return_validator.call(result)
129
+ raise ReturnContractError, "Invalid return value for #{decorator_printed_name}\n #{
130
+ return_validator.message(result, 'returns', 0).strip
131
+ }"
132
+ end
133
+
134
+ result
135
+ end
119
136
  end
120
137
 
121
- def make_definition
138
+ def apply_contract_wrapper_directly
139
+ # Legacy mode for Contractable standalone usage
140
+ # Detect visibility BEFORE creating alias
141
+ visibility = if alias_target.private_instance_methods.include?(method_name)
142
+ :private
143
+ elsif alias_target.protected_instance_methods.include?(method_name)
144
+ :protected
145
+ else
146
+ :public
147
+ end
148
+
149
+ # Create our own alias and wrapper
150
+ method_alias = :"__original_#{method_name}_#{SecureRandom.hex}"
151
+ alias_target.alias_method(method_alias, method_name)
152
+
153
+ args_validator = @args
154
+ return_validator = @return_validator
155
+ decorator_printed_name = printed_name
156
+
122
157
  file, line = alias_target.instance_method(method_alias).source_location
123
158
 
124
159
  alias_target.class_eval(%Q(
125
160
  def #{method_name}(*args, **kwargs, &blk)
126
161
  decorator = Ree::Contracts::MethodDecorator.get_decorator('#{id}')
127
- decorator.execute_on(self, args, kwargs, &blk)
162
+ decorator.validate_and_call(self, #{method_alias.inspect}, args, kwargs, &blk)
128
163
  end
129
164
  ), file, line - 3)
130
165
 
131
- make_private if private_method?
132
- make_protected if protected_method?
166
+ # Restore visibility
167
+ case visibility
168
+ when :private
169
+ alias_target.send(:private, method_name, method_alias)
170
+ when :protected
171
+ alias_target.send(:protected, method_name, method_alias)
172
+ end
133
173
  end
134
174
 
135
175
  def private_method?
136
- return target.private_methods.include?(method_alias) if is_class_method
137
- target.private_instance_methods.include?(method_alias)
176
+ return target.private_methods.include?(method_name) if is_class_method
177
+ target.private_instance_methods.include?(method_name)
138
178
  end
139
179
 
140
180
  def protected_method?
141
- return target.protected_methods.include?(method_alias) if is_class_method
142
- target.protected_instance_methods.include?(method_alias)
143
- end
144
-
145
- def make_private
146
- _method_name = method_name
147
- _method_alias = method_alias
148
-
149
- alias_target.class_eval do
150
- private _method_name
151
- private _method_alias
152
- end
153
- end
154
-
155
- def make_protected
156
- _method_name = method_name
157
- _method_alias = method_alias
158
-
159
- alias_target.class_eval do
160
- protected _method_name
161
- protected _method_alias
162
- end
181
+ return target.protected_methods.include?(method_name) if is_class_method
182
+ target.protected_instance_methods.include?(method_name)
163
183
  end
164
184
 
165
185
  def printed_name
@@ -95,7 +95,22 @@ class Ree::PackageLoader
95
95
 
96
96
  Ree.logger.debug("load_file(:#{package_name}, '#{path}')")
97
97
 
98
- Kernel.require(path)
98
+ if Ree.obfuscated?
99
+ load_encrypted_file(path)
100
+ else
101
+ Kernel.require(path)
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def load_encrypted_file(path)
108
+ license = Ree.license
109
+ full_path = path.end_with?('.rb') ? path : "#{path}.rb"
110
+ encrypted_data = File.binread(full_path)
111
+ bytecode = license.decrypt_file(encrypted_data)
112
+ iseq = RubyVM::InstructionSequence.load_from_binary(bytecode)
113
+ iseq.eval
99
114
  end
100
115
  end
101
116
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ree
4
+ module Licensing
5
+ class BytecodeCompiler
6
+ def self.compile_file(path)
7
+ iseq = RubyVM::InstructionSequence.compile_file(path)
8
+ iseq.to_binary
9
+ end
10
+
11
+ def self.compile_string(source, path = "(eval)")
12
+ iseq = RubyVM::InstructionSequence.compile(source, path)
13
+ iseq.to_binary
14
+ end
15
+
16
+ def self.load_from_binary(binary)
17
+ RubyVM::InstructionSequence.load_from_binary(binary)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'openssl'
6
+
7
+ module Ree
8
+ module Licensing
9
+ class ClientStore
10
+ CLIENTS_FILE = 'clients.json'
11
+
12
+ def initialize(dir)
13
+ @dir = dir
14
+ @file_path = File.join(dir, CLIENTS_FILE)
15
+ end
16
+
17
+ def register_client(name:, contact:, metadata: {})
18
+ data = load_data
19
+ client_id = "client_#{SecureRandom.hex(8)}"
20
+ rsa_key = OpenSSL::PKey::RSA.new(4096)
21
+
22
+ client = {
23
+ 'client_id' => client_id,
24
+ 'name' => name,
25
+ 'contact' => contact,
26
+ 'metadata' => metadata,
27
+ 'created_at' => Date.today.to_s,
28
+ 'private_key_pem' => rsa_key.to_pem,
29
+ 'public_key_pem' => rsa_key.public_key.to_pem,
30
+ 'licenses' => []
31
+ }
32
+
33
+ data['clients'] << client
34
+ save_data(data)
35
+
36
+ client
37
+ end
38
+
39
+ def find_client(client_id)
40
+ data = load_data
41
+ data['clients'].detect { |c| c['client_id'] == client_id }
42
+ end
43
+
44
+ def add_license(client_id, license)
45
+ data = load_data
46
+ client = data['clients'].detect { |c| c['client_id'] == client_id }
47
+ raise Ree::Error.new("Client #{client_id} not found") unless client
48
+
49
+ client['licenses'] << license
50
+ save_data(data)
51
+ end
52
+
53
+ def last_license(client_id)
54
+ client = find_client(client_id)
55
+ raise Ree::Error.new("Client #{client_id} not found") unless client
56
+
57
+ client['licenses'].last
58
+ end
59
+
60
+ def load_data
61
+ if File.exist?(@file_path)
62
+ JSON.parse(File.read(@file_path))
63
+ else
64
+ { 'clients' => [] }
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def save_data(data)
71
+ File.write(@file_path, JSON.pretty_generate(data))
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'base64'
5
+ require 'openssl'
6
+ require 'date'
7
+
8
+ module Ree
9
+ module Licensing
10
+ class Decryptor
11
+ attr_reader :aes_key, :iv, :expires_at, :client_id
12
+
13
+ def initialize(aes_key:, iv:, expires_at:, client_id:)
14
+ @aes_key = aes_key
15
+ @iv = iv
16
+ @expires_at = expires_at
17
+ @client_id = client_id
18
+ end
19
+
20
+ def self.load_license(license_path)
21
+ unless File.exist?(license_path)
22
+ raise Ree::Error.new("License file not found: #{license_path}")
23
+ end
24
+
25
+ license_data = JSON.parse(File.read(license_path))
26
+ public_key_pem = license_data['public_key_pem']
27
+ encrypted_payload = Base64.strict_decode64(license_data['encrypted_payload'])
28
+
29
+ payload_json = Encryptor.rsa_public_decrypt(encrypted_payload, public_key_pem)
30
+ payload = JSON.parse(payload_json)
31
+
32
+ expires_at = Date.parse(payload['expires_at'])
33
+
34
+ if expires_at < Date.today
35
+ raise Ree::Error.new("License expired on #{payload['expires_at']}")
36
+ end
37
+
38
+ aes_key = [payload['aes_key_hex']].pack('H*')
39
+ iv = [payload['iv_hex']].pack('H*')
40
+
41
+ new(
42
+ aes_key: aes_key,
43
+ iv: iv,
44
+ expires_at: expires_at,
45
+ client_id: payload['client_id']
46
+ )
47
+ end
48
+
49
+ def decrypt_file(encrypted_data)
50
+ Encryptor.aes_decrypt(encrypted_data, @aes_key, @iv)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'securerandom'
5
+ require 'json'
6
+ require 'base64'
7
+
8
+ module Ree
9
+ module Licensing
10
+ class Encryptor
11
+ def self.generate_aes_key
12
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
13
+ cipher.encrypt
14
+ {
15
+ key: cipher.random_key,
16
+ iv: cipher.random_iv
17
+ }
18
+ end
19
+
20
+ def self.aes_encrypt(data, key, iv)
21
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
22
+ cipher.encrypt
23
+ cipher.key = key
24
+ cipher.iv = iv
25
+ cipher.update(data) + cipher.final
26
+ end
27
+
28
+ def self.aes_decrypt(encrypted_data, key, iv)
29
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
30
+ cipher.decrypt
31
+ cipher.key = key
32
+ cipher.iv = iv
33
+ cipher.update(encrypted_data) + cipher.final
34
+ end
35
+
36
+ def self.rsa_private_encrypt(payload_json, private_key_pem)
37
+ rsa = OpenSSL::PKey::RSA.new(private_key_pem)
38
+ rsa.private_encrypt(payload_json)
39
+ end
40
+
41
+ def self.rsa_public_decrypt(encrypted_payload, public_key_pem)
42
+ rsa = OpenSSL::PKey::RSA.new(public_key_pem)
43
+ rsa.public_decrypt(encrypted_payload)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'base64'
5
+ require 'securerandom'
6
+
7
+ module Ree
8
+ module Licensing
9
+ class LicenseGenerator
10
+ def self.generate(client_id:, private_key_pem:, public_key_pem:, aes_key_hex:, iv_hex:, expires_at:)
11
+ payload = {
12
+ 'aes_key_hex' => aes_key_hex,
13
+ 'iv_hex' => iv_hex,
14
+ 'expires_at' => expires_at,
15
+ 'client_id' => client_id
16
+ }
17
+
18
+ payload_json = JSON.generate(payload)
19
+ encrypted_payload = Encryptor.rsa_private_encrypt(payload_json, private_key_pem)
20
+
21
+ license_file = {
22
+ 'version' => 1,
23
+ 'client_id' => client_id,
24
+ 'public_key_pem' => public_key_pem,
25
+ 'encrypted_payload' => Base64.strict_encode64(encrypted_payload)
26
+ }
27
+
28
+ license_record = {
29
+ 'license_id' => "lic_#{SecureRandom.hex(6)}",
30
+ 'expires_at' => expires_at,
31
+ 'aes_key_hex' => aes_key_hex,
32
+ 'iv_hex' => iv_hex,
33
+ 'created_at' => Date.today.to_s
34
+ }
35
+
36
+ { license_file: license_file, license_record: license_record }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'date'
6
+
7
+ module Ree
8
+ module Licensing
9
+ class Obfuscator
10
+ def self.run(source_path:, target_path:, client_id:, expires_at:, clients_dir:, exclude_files: [], stdout: $stdout)
11
+ new(
12
+ source_path: source_path,
13
+ target_path: target_path,
14
+ client_id: client_id,
15
+ expires_at: expires_at,
16
+ clients_dir: clients_dir,
17
+ exclude_files: exclude_files,
18
+ stdout: stdout
19
+ ).run
20
+ end
21
+
22
+ def initialize(source_path:, target_path:, client_id:, expires_at:, clients_dir:, exclude_files: [], stdout: $stdout)
23
+ @source_path = File.expand_path(source_path)
24
+ @target_path = File.expand_path(target_path)
25
+ @client_id = client_id
26
+ @expires_at = expires_at
27
+ @clients_dir = clients_dir
28
+ @exclude_files = exclude_files
29
+ @stdout = stdout
30
+ end
31
+
32
+ def run
33
+ start_time = Time.now
34
+
35
+ store = ClientStore.new(@clients_dir)
36
+ client = store.find_client(@client_id)
37
+ raise Ree::Error.new("Client #{@client_id} not found in clients.json") unless client
38
+
39
+ copy_project
40
+ remove_spec_dirs
41
+
42
+ aes_data = Encryptor.generate_aes_key
43
+ aes_key = aes_data[:key]
44
+ iv = aes_data[:iv]
45
+ aes_key_hex = aes_key.unpack1('H*')
46
+ iv_hex = iv.unpack1('H*')
47
+
48
+ encrypted_count = encrypt_ruby_files(aes_key, iv)
49
+
50
+ result = LicenseGenerator.generate(
51
+ client_id: @client_id,
52
+ private_key_pem: client['private_key_pem'],
53
+ public_key_pem: client['public_key_pem'],
54
+ aes_key_hex: aes_key_hex,
55
+ iv_hex: iv_hex,
56
+ expires_at: @expires_at
57
+ )
58
+
59
+ license_path = File.join(@target_path, 'license.json')
60
+ File.write(license_path, JSON.pretty_generate(result[:license_file]))
61
+
62
+ store.add_license(@client_id, result[:license_record])
63
+
64
+ elapsed = (Time.now - start_time).round(2)
65
+ @stdout.puts "Obfuscation complete:"
66
+ @stdout.puts " Files encrypted: #{encrypted_count}"
67
+ @stdout.puts " License file: #{license_path}"
68
+ @stdout.puts " Time: #{elapsed}s"
69
+
70
+ { encrypted_count: encrypted_count, license_path: license_path }
71
+ end
72
+
73
+ private
74
+
75
+ def copy_project
76
+ FileUtils.rm_rf(@target_path) if Dir.exist?(@target_path)
77
+ FileUtils.cp_r(@source_path, @target_path)
78
+ end
79
+
80
+ def remove_spec_dirs
81
+ Dir.glob(File.join(@target_path, '**/spec')).each do |spec_dir|
82
+ FileUtils.rm_rf(spec_dir) if File.directory?(spec_dir)
83
+ end
84
+ end
85
+
86
+ def encrypt_ruby_files(aes_key, iv)
87
+ count = 0
88
+ pattern = File.join(@target_path, '**/package/**/*.rb')
89
+
90
+ Dir.glob(pattern).each do |file_path|
91
+ basename = File.basename(file_path)
92
+ next if @exclude_files.include?(basename)
93
+
94
+ source = File.read(file_path)
95
+ if source.include?('require_relative')
96
+ raise Ree::Error.new("File contains require_relative: #{file_path}")
97
+ end
98
+
99
+ bytecode = BytecodeCompiler.compile_file(file_path)
100
+ encrypted = Encryptor.aes_encrypt(bytecode, aes_key, iv)
101
+ File.binwrite(file_path, encrypted)
102
+ count += 1
103
+ end
104
+
105
+ count
106
+ end
107
+ end
108
+ end
109
+ end
@@ -8,14 +8,22 @@ module Ree::MethodAddedHook
8
8
 
9
9
  @__ree_plugin_running = true
10
10
 
11
- original_alias = :"__ree_original_#{name}"
12
- remove_method(original_alias) if method_defined?(original_alias)
13
- alias_method(original_alias, name)
14
-
15
11
  begin
16
- plugins.each do |plugin_class|
17
- plugin_class.new(name, false, self).call
12
+ # Collect wrapper lambdas from plugins (some may return nil)
13
+ wrappers = plugins.map { |plugin_class| plugin_class.new(name, false, self).call }.compact
14
+
15
+ if wrappers.empty?
16
+ @__ree_plugin_running = false
17
+ return super
18
18
  end
19
+
20
+ # Create single alias for original implementation
21
+ original_alias = :"__ree_original_#{name}"
22
+ remove_method(original_alias) if method_defined?(original_alias)
23
+ alias_method(original_alias, name)
24
+
25
+ # Compose all wrappers into a single method
26
+ compose_method(name, original_alias, wrappers)
19
27
  ensure
20
28
  @__ree_plugin_running = false
21
29
  end
@@ -30,22 +38,106 @@ module Ree::MethodAddedHook
30
38
 
31
39
  @__ree_singleton_plugin_running = true
32
40
 
33
- original_alias = :"__ree_original_#{name}"
34
41
  eigenclass = class << self; self; end
35
42
 
36
- if eigenclass.method_defined?(original_alias)
37
- eigenclass.remove_method(original_alias)
38
- end
39
- eigenclass.alias_method(original_alias, name)
40
-
41
43
  begin
42
- plugins.each do |plugin_class|
43
- plugin_class.new(name, true, self).call
44
+ # Collect wrapper lambdas from plugins (some may return nil)
45
+ wrappers = plugins.map { |plugin_class| plugin_class.new(name, true, self).call }.compact
46
+
47
+ if wrappers.empty?
48
+ @__ree_singleton_plugin_running = false
49
+ return super
44
50
  end
51
+
52
+ # Create single alias for original implementation
53
+ original_alias = :"__ree_original_#{name}"
54
+ eigenclass.remove_method(original_alias) if eigenclass.method_defined?(original_alias)
55
+ eigenclass.alias_method(original_alias, name)
56
+
57
+ # Compose all wrappers into a single method
58
+ compose_singleton_method(eigenclass, name, original_alias, wrappers)
45
59
  ensure
46
60
  @__ree_singleton_plugin_running = false
47
61
  end
48
62
 
49
63
  super
50
64
  end
65
+
66
+ private
67
+
68
+ def compose_method(method_name, original_alias, wrappers)
69
+ # Detect original method visibility
70
+ visibility = if private_method_defined?(original_alias)
71
+ :private
72
+ elsif protected_method_defined?(original_alias)
73
+ :protected
74
+ else
75
+ :public
76
+ end
77
+
78
+ # Build executor chain from inside out
79
+ define_method(method_name) do |*args, **kwargs, &block|
80
+ # Innermost layer: call the original method
81
+ # Note: next_layer signature is: ->(){ ... } to be called with .call(*args, **kwargs, &block)
82
+ executor = ->(*a, **kw, &b) { send(original_alias, *a, **kw, &b) }
83
+
84
+ # Wrap from inside out (reverse to make first plugin outermost)
85
+ wrappers.reverse_each do |wrapper|
86
+ current_executor = executor
87
+ # Wrapper receives: (instance, next_layer, *args, **kwargs, &block)
88
+ # We need to create a next_layer that calls current_executor
89
+ next_layer = ->(*a, **kw, &b) { current_executor.call(*a, **kw, &b) }
90
+ executor = ->(*a, **kw, &b) { wrapper.call(self, next_layer, *a, **kw, &b) }
91
+ end
92
+
93
+ # Execute the composed chain
94
+ executor.call(*args, **kwargs, &block)
95
+ end
96
+
97
+ # Restore original visibility
98
+ case visibility
99
+ when :private
100
+ private method_name
101
+ when :protected
102
+ protected method_name
103
+ end
104
+ end
105
+
106
+ def compose_singleton_method(eigenclass, method_name, original_alias, wrappers)
107
+ # Detect original method visibility
108
+ visibility = if eigenclass.private_method_defined?(original_alias)
109
+ :private
110
+ elsif eigenclass.protected_method_defined?(original_alias)
111
+ :protected
112
+ else
113
+ :public
114
+ end
115
+
116
+ # Build executor chain from inside out
117
+ eigenclass.define_method(method_name) do |*args, **kwargs, &block|
118
+ # Innermost layer: call the original method
119
+ # Note: next_layer signature is: ->(){ ... } to be called with .call(*args, **kwargs, &block)
120
+ executor = ->(*a, **kw, &b) { send(original_alias, *a, **kw, &b) }
121
+
122
+ # Wrap from inside out (reverse to make first plugin outermost)
123
+ wrappers.reverse_each do |wrapper|
124
+ current_executor = executor
125
+ # Wrapper receives: (instance, next_layer, *args, **kwargs, &block)
126
+ # We need to create a next_layer that calls current_executor
127
+ next_layer = ->(*a, **kw, &b) { current_executor.call(*a, **kw, &b) }
128
+ executor = ->(*a, **kw, &b) { wrapper.call(self, next_layer, *a, **kw, &b) }
129
+ end
130
+
131
+ # Execute the composed chain
132
+ executor.call(*args, **kwargs, &block)
133
+ end
134
+
135
+ # Restore original visibility
136
+ case visibility
137
+ when :private
138
+ eigenclass.send(:private, method_name)
139
+ when :protected
140
+ eigenclass.send(:protected, method_name)
141
+ end
142
+ end
51
143
  end
data/lib/ree/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ree
4
- VERSION = "1.2.4"
4
+ VERSION = "1.2.7"
5
5
  end
data/lib/ree.rb CHANGED
@@ -53,6 +53,15 @@ module Ree
53
53
  autoload :TemplateHandler, 'ree/handlers/template_handler'
54
54
  autoload :TemplateRenderer, 'ree/templates/template_renderer'
55
55
 
56
+ module Licensing
57
+ autoload :ClientStore, 'ree/licensing/client_store'
58
+ autoload :Encryptor, 'ree/licensing/encryptor'
59
+ autoload :Decryptor, 'ree/licensing/decryptor'
60
+ autoload :BytecodeCompiler, 'ree/licensing/bytecode_compiler'
61
+ autoload :LicenseGenerator, 'ree/licensing/license_generator'
62
+ autoload :Obfuscator, 'ree/licensing/obfuscator'
63
+ end
64
+
56
65
  PACKAGE = 'package'
57
66
  SCHEMAS = 'schemas'
58
67
  SCHEMA = 'schema'
@@ -124,6 +133,20 @@ module Ree
124
133
  !!@benchmark_mode
125
134
  end
126
135
 
136
+ def obfuscated?
137
+ ENV.has_key?('REE_LICENSE_KEY')
138
+ end
139
+
140
+ def license
141
+ @license ||= if obfuscated?
142
+ Ree::Licensing::Decryptor.load_license(ENV['REE_LICENSE_KEY'])
143
+ end
144
+ end
145
+
146
+ def reset_license
147
+ @license = nil
148
+ end
149
+
127
150
  def method_added_plugins
128
151
  @method_added_plugins ||= []
129
152
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ree
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.4
4
+ version: 1.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Gatiyatov
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: 1.6.5
40
+ - !ruby/object:Gem::Dependency
41
+ name: base64
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: debug
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +100,9 @@ files:
86
100
  - lib/ree/cli/indexing/index_package.rb
87
101
  - lib/ree/cli/indexing/index_project.rb
88
102
  - lib/ree/cli/init.rb
103
+ - lib/ree/cli/licensing/obfuscate.rb
104
+ - lib/ree/cli/licensing/register_client.rb
105
+ - lib/ree/cli/licensing/renew_license.rb
89
106
  - lib/ree/cli/spec_runner.rb
90
107
  - lib/ree/container.rb
91
108
  - lib/ree/contracts.rb
@@ -166,6 +183,12 @@ files:
166
183
  - lib/ree/gen/package.rb
167
184
  - lib/ree/handlers/template_handler.rb
168
185
  - lib/ree/inspectable.rb
186
+ - lib/ree/licensing/bytecode_compiler.rb
187
+ - lib/ree/licensing/client_store.rb
188
+ - lib/ree/licensing/decryptor.rb
189
+ - lib/ree/licensing/encryptor.rb
190
+ - lib/ree/licensing/license_generator.rb
191
+ - lib/ree/licensing/obfuscator.rb
169
192
  - lib/ree/link_dsl.rb
170
193
  - lib/ree/method_added_hook.rb
171
194
  - lib/ree/object_compiler.rb