hyraft 0.1.0.alpha1
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +231 -0
- data/exe/hyraft +5 -0
- data/lib/hyraft/boot/asset_preloader.rb +185 -0
- data/lib/hyraft/boot/preloaded_static.rb +46 -0
- data/lib/hyraft/boot/preloader.rb +206 -0
- data/lib/hyraft/cli.rb +187 -0
- data/lib/hyraft/compiler/compiler.rb +34 -0
- data/lib/hyraft/compiler/html_purifier.rb +181 -0
- data/lib/hyraft/compiler/javascript_library.rb +281 -0
- data/lib/hyraft/compiler/javascript_obfuscator.rb +141 -0
- data/lib/hyraft/compiler/parser.rb +27 -0
- data/lib/hyraft/compiler/renderer.rb +217 -0
- data/lib/hyraft/engine/circuit.rb +35 -0
- data/lib/hyraft/engine/port.rb +17 -0
- data/lib/hyraft/engine/source.rb +19 -0
- data/lib/hyraft/engine.rb +11 -0
- data/lib/hyraft/router/api_router.rb +65 -0
- data/lib/hyraft/router/web_router.rb +136 -0
- data/lib/hyraft/system_info.rb +26 -0
- data/lib/hyraft/version.rb +5 -0
- data/lib/hyraft.rb +48 -0
- data/templates/do_app/Gemfile +50 -0
- data/templates/do_app/Rakefile +88 -0
- data/templates/do_app/adapter-intake/web-app/display/pages/home/home.hyr +174 -0
- data/templates/do_app/adapter-intake/web-app/request/home_web_adapter.rb +19 -0
- data/templates/do_app/boot.rb +41 -0
- data/templates/do_app/framework/adapters/server/server_api_adapter.rb +51 -0
- data/templates/do_app/framework/adapters/server/server_web_adapter.rb +178 -0
- data/templates/do_app/framework/compiler/style_resolver.rb +33 -0
- data/templates/do_app/framework/errors/error_handler.rb +75 -0
- data/templates/do_app/framework/errors/templates/304.html +22 -0
- data/templates/do_app/framework/errors/templates/400.html +22 -0
- data/templates/do_app/framework/errors/templates/401.html +22 -0
- data/templates/do_app/framework/errors/templates/403.html +22 -0
- data/templates/do_app/framework/errors/templates/404.html +62 -0
- data/templates/do_app/framework/errors/templates/500.html +73 -0
- data/templates/do_app/framework/middleware/cors_middleware.rb +37 -0
- data/templates/do_app/infra/config/environment.rb +86 -0
- data/templates/do_app/infra/config/error_config.rb +80 -0
- data/templates/do_app/infra/config/routes/api_routes.rb +2 -0
- data/templates/do_app/infra/config/routes/web_routes.rb +10 -0
- data/templates/do_app/infra/database/sequel_connection.rb +62 -0
- data/templates/do_app/infra/gems/database.rb +7 -0
- data/templates/do_app/infra/gems/load_all.rb +4 -0
- data/templates/do_app/infra/gems/utilities.rb +1 -0
- data/templates/do_app/infra/gems/web.rb +3 -0
- data/templates/do_app/infra/server/api-server.ru +13 -0
- data/templates/do_app/infra/server/web-server.ru +32 -0
- data/templates/do_app/package.json +9 -0
- data/templates/do_app/public/favicon.ico +0 -0
- data/templates/do_app/public/icons/docs.svg +10 -0
- data/templates/do_app/public/icons/expli.svg +13 -0
- data/templates/do_app/public/icons/git-repo.svg +13 -0
- data/templates/do_app/public/icons/hexagonal-arch.svg +15 -0
- data/templates/do_app/public/icons/template-engine.svg +26 -0
- data/templates/do_app/public/images/hyr-logo.png +0 -0
- data/templates/do_app/public/images/hyr-logo.webp +0 -0
- data/templates/do_app/public/index.html +22 -0
- data/templates/do_app/public/styles/css/main.css +418 -0
- data/templates/do_app/public/styles/css/spa.css +171 -0
- data/templates/do_app/shared/helpers/pagination_helper.rb +44 -0
- data/templates/do_app/shared/helpers/response_formatter.rb +25 -0
- data/templates/do_app/test/acceptance/api/articles_api_acceptance_test.rb +43 -0
- data/templates/do_app/test/acceptance/web/articles_acceptance_test.rb +31 -0
- data/templates/do_app/test/acceptance/web/home_acceptance_test.rb +17 -0
- data/templates/do_app/test/db.rb +106 -0
- data/templates/do_app/test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb +79 -0
- data/templates/do_app/test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb +61 -0
- data/templates/do_app/test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb +20 -0
- data/templates/do_app/test/integration/adapter-intake/web-app/request/home_web_adapter_test.rb +17 -0
- data/templates/do_app/test/integration/database/migration_test.rb +35 -0
- data/templates/do_app/test/support/mock_api_adapter.rb +82 -0
- data/templates/do_app/test/support/mock_articles_gateway.rb +41 -0
- data/templates/do_app/test/support/mock_web_adapter.rb +85 -0
- data/templates/do_app/test/support/test_patches.rb +33 -0
- data/templates/do_app/test/test_helper.rb +526 -0
- data/templates/do_app/test/unit/engine/circuit/articles_circuit_test.rb +167 -0
- data/templates/do_app/test/unit/engine/port/articles_gateway_port_test.rb +12 -0
- data/templates/do_app/test/unit/engine/source/article_test.rb +37 -0
- metadata +291 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# lib/hyraft/compiler/javascript_library.rb
|
|
2
|
+
require_relative 'javascript_obfuscator'
|
|
3
|
+
|
|
4
|
+
module Hyraft
|
|
5
|
+
module Compiler
|
|
6
|
+
class JavaScriptLibrary
|
|
7
|
+
# Store the original clean version
|
|
8
|
+
CLEAN_LIBRARIES = {
|
|
9
|
+
'lib/neonpulse' => <<~JAVASCRIPT
|
|
10
|
+
/* NeonPulse Library */
|
|
11
|
+
(() => {
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
class NeonPulse {
|
|
15
|
+
#signals = new Map();
|
|
16
|
+
#outputs = new Map();
|
|
17
|
+
#processors = new Map();
|
|
18
|
+
#forms = new Map();
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
this.#initialize();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#initialize = () => {
|
|
25
|
+
const readyHandler = () => {
|
|
26
|
+
this.#connectOutputs();
|
|
27
|
+
this.#connectForms();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
document.readyState === 'loading'
|
|
31
|
+
? document.addEventListener('DOMContentLoaded', readyHandler)
|
|
32
|
+
: readyHandler();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
#connectOutputs = () => {
|
|
36
|
+
const signalElements = [...document.querySelectorAll('[data-neon]')];
|
|
37
|
+
const actionElements = [...document.querySelectorAll('[data-pulse]')];
|
|
38
|
+
|
|
39
|
+
signalElements.forEach(element => {
|
|
40
|
+
const signalName = element.dataset.neon;
|
|
41
|
+
const property = element.dataset.property || 'textContent';
|
|
42
|
+
|
|
43
|
+
if (!this.#outputs.has(signalName)) {
|
|
44
|
+
this.#outputs.set(signalName, []);
|
|
45
|
+
}
|
|
46
|
+
this.#outputs.get(signalName).push({ element, property });
|
|
47
|
+
|
|
48
|
+
const signal = this.#signals.get(signalName);
|
|
49
|
+
signal && this.#updateElement(element, signal.value, property);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
actionElements.forEach(element => {
|
|
53
|
+
const [processor, action] = element.dataset.pulse?.split('.') ?? [];
|
|
54
|
+
const eventType = element.dataset.event || 'click';
|
|
55
|
+
|
|
56
|
+
element.addEventListener(eventType, event => {
|
|
57
|
+
window[processor]?.[action]?.(event);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
#connectForms = () => {
|
|
63
|
+
const formElements = [...document.querySelectorAll('form[data-neon-form]')];
|
|
64
|
+
|
|
65
|
+
formElements.forEach(form => {
|
|
66
|
+
const formName = form.dataset.neonForm;
|
|
67
|
+
const [processor, action] = form.dataset.submit?.split('.') ?? [];
|
|
68
|
+
|
|
69
|
+
if (processor && action) {
|
|
70
|
+
form.addEventListener('submit', event => {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
event.stopPropagation();
|
|
73
|
+
window[processor]?.[action]?.(this.#getFormData(form), event);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.#forms.set(formName, form);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.#bindFormInputs(form);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
#bindFormInputs = form => {
|
|
84
|
+
const inputs = form.querySelectorAll('input, textarea, select');
|
|
85
|
+
|
|
86
|
+
inputs.forEach(input => {
|
|
87
|
+
const signalName = input.dataset.neon;
|
|
88
|
+
if (!signalName || !this.#signals.has(signalName)) return;
|
|
89
|
+
|
|
90
|
+
const signal = this.#signals.get(signalName);
|
|
91
|
+
|
|
92
|
+
// Set initial value from signal
|
|
93
|
+
if (!['checkbox', 'radio'].includes(input.type)) {
|
|
94
|
+
input.value = signal.value ?? '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Bind input events to update signals
|
|
98
|
+
input.addEventListener('input', () => {
|
|
99
|
+
if (input.type === 'checkbox') {
|
|
100
|
+
signal.value = input.checked;
|
|
101
|
+
} else if (input.type === 'radio') {
|
|
102
|
+
if (input.checked) signal.value = input.value;
|
|
103
|
+
} else {
|
|
104
|
+
signal.value = input.value;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Bind signal changes to update input
|
|
109
|
+
const unwatch = this.watch(signalName, newValue => {
|
|
110
|
+
if (input.type === 'checkbox') {
|
|
111
|
+
input.checked = !!newValue;
|
|
112
|
+
} else if (input.type === 'radio') {
|
|
113
|
+
input.checked = (input.value === newValue);
|
|
114
|
+
} else {
|
|
115
|
+
input.value = newValue ?? '';
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Store unwatch function for cleanup
|
|
120
|
+
if (!this.#forms.has('_watchers')) {
|
|
121
|
+
this.#forms.set('_watchers', new Map());
|
|
122
|
+
}
|
|
123
|
+
this.#forms.get('_watchers').set(input, unwatch);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
#getFormData = form => {
|
|
128
|
+
const formData = {};
|
|
129
|
+
const inputs = form.querySelectorAll('input, textarea, select');
|
|
130
|
+
|
|
131
|
+
inputs.forEach(input => {
|
|
132
|
+
const name = input.name || input.id;
|
|
133
|
+
if (!name) return;
|
|
134
|
+
|
|
135
|
+
if (input.type === 'checkbox') {
|
|
136
|
+
formData[name] = input.checked;
|
|
137
|
+
} else if (input.type === 'radio') {
|
|
138
|
+
if (input.checked) formData[name] = input.value;
|
|
139
|
+
} else if (input.type === 'select-multiple') {
|
|
140
|
+
formData[name] = [...input.selectedOptions].map(opt => opt.value);
|
|
141
|
+
} else {
|
|
142
|
+
formData[name] = input.value;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return formData;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
#emitSignal = signalName => {
|
|
150
|
+
const connectedElements = this.#outputs.get(signalName);
|
|
151
|
+
const signal = this.#signals.get(signalName);
|
|
152
|
+
|
|
153
|
+
connectedElements?.forEach(({ element, property }) => {
|
|
154
|
+
this.#updateElement(element, signal.value, property);
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
#updateElement = (element, value, property) => {
|
|
159
|
+
if (!element) return;
|
|
160
|
+
|
|
161
|
+
const updates = {
|
|
162
|
+
class: () => element.className = value,
|
|
163
|
+
style: () => element.style.cssText = value,
|
|
164
|
+
value: () => {
|
|
165
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
166
|
+
element.value = value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
updates[property]?.() ?? (element[property] = value);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Public API
|
|
175
|
+
neon = (signalName, initialValue) => {
|
|
176
|
+
const proxy = new Proxy({ value: initialValue }, {
|
|
177
|
+
set: (target, property, value) => {
|
|
178
|
+
const success = Reflect.set(target, property, value);
|
|
179
|
+
if (success && property === 'value') {
|
|
180
|
+
this.#emitSignal(signalName);
|
|
181
|
+
}
|
|
182
|
+
return success;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
this.#signals.set(signalName, proxy);
|
|
187
|
+
return proxy;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
pulse = (name, actions) => {
|
|
191
|
+
const boundActions = Object.fromEntries(
|
|
192
|
+
Object.entries(actions).map(([key, value]) => [
|
|
193
|
+
key,
|
|
194
|
+
typeof value === 'function' ? value.bind(actions) : value
|
|
195
|
+
])
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
window[name] = boundActions;
|
|
199
|
+
return boundActions;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
neonBatch = signalsObj => {
|
|
203
|
+
const results = {};
|
|
204
|
+
Object.keys(signalsObj).forEach(key => {
|
|
205
|
+
results[key] = this.neon(key, signalsObj[key]);
|
|
206
|
+
});
|
|
207
|
+
return results;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
watch = (signalName, callback) => {
|
|
211
|
+
const signal = this.#signals.get(signalName);
|
|
212
|
+
if (!signal) return;
|
|
213
|
+
|
|
214
|
+
let previousValue = signal.value;
|
|
215
|
+
const watcher = setInterval(() => {
|
|
216
|
+
if (signal.value !== previousValue) {
|
|
217
|
+
callback(signal.value, previousValue);
|
|
218
|
+
previousValue = signal.value;
|
|
219
|
+
}
|
|
220
|
+
}, 100);
|
|
221
|
+
|
|
222
|
+
return () => clearInterval(watcher);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
form = (formName, initialData) => {
|
|
226
|
+
const formData = this.neonBatch(initialData ?? {});
|
|
227
|
+
return formData;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
submit = (formName, handler) => {
|
|
231
|
+
const form = this.#forms.get(formName);
|
|
232
|
+
if (form && handler) {
|
|
233
|
+
form.addEventListener('submit', event => {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
const formData = this.#getFormData(form);
|
|
236
|
+
handler(formData, event);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
cleanup = () => {
|
|
242
|
+
if (this.#forms.has('_watchers')) {
|
|
243
|
+
this.#forms.get('_watchers').forEach(unwatch => {
|
|
244
|
+
typeof unwatch === 'function' && unwatch();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
window.neonPulse = new NeonPulse();
|
|
251
|
+
// console.log('🚀 NeonPulse activated);
|
|
252
|
+
})();
|
|
253
|
+
JAVASCRIPT
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def self.get(library_name, obfuscation_method: :multi_layer)
|
|
257
|
+
clean_code = CLEAN_LIBRARIES[library_name]
|
|
258
|
+
return nil unless clean_code
|
|
259
|
+
|
|
260
|
+
case obfuscation_method
|
|
261
|
+
when :split_and_reassemble
|
|
262
|
+
JavaScriptObfuscator.split_and_reassemble(clean_code)
|
|
263
|
+
when :multi_layer
|
|
264
|
+
JavaScriptObfuscator.multi_layer_obfuscation(clean_code)
|
|
265
|
+
when :none
|
|
266
|
+
clean_code
|
|
267
|
+
else
|
|
268
|
+
JavaScriptObfuscator.multi_layer_obfuscation(clean_code)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def self.available_libraries
|
|
273
|
+
CLEAN_LIBRARIES.keys
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def self.obfuscation_methods
|
|
277
|
+
[:multi_layer, :split_and_reassemble, :none]
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# lib/hyraft/compiler/javascript_obfuscator.rb
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module Hyraft
|
|
5
|
+
module Compiler
|
|
6
|
+
class JavaScriptObfuscator
|
|
7
|
+
class << self
|
|
8
|
+
# Method 4: Split and Reassemble
|
|
9
|
+
def split_and_reassemble(js_code, parts: 8)
|
|
10
|
+
return '' unless js_code && !js_code.strip.empty?
|
|
11
|
+
|
|
12
|
+
# Calculate chunk size
|
|
13
|
+
chunk_size = (js_code.length.to_f / parts).ceil
|
|
14
|
+
# Split into chunks
|
|
15
|
+
chunks = js_code.chars.each_slice(chunk_size).map(&:join)
|
|
16
|
+
|
|
17
|
+
# Convert to JSON for JavaScript array
|
|
18
|
+
chunks_js = chunks.to_json
|
|
19
|
+
|
|
20
|
+
<<~JAVASCRIPT
|
|
21
|
+
(function(){
|
|
22
|
+
try {
|
|
23
|
+
var c=#{chunks_js};
|
|
24
|
+
var s=c.join('');
|
|
25
|
+
var e=document.createElement('script');
|
|
26
|
+
e.textContent=s;
|
|
27
|
+
document.head.appendChild(e);
|
|
28
|
+
} catch(err) {
|
|
29
|
+
console.error('Script load failed:', err);
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
JAVASCRIPT
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Method 10: Multi-Layer Obfuscation
|
|
36
|
+
def multi_layer_obfuscation(js_code)
|
|
37
|
+
return '' unless js_code && !js_code.strip.empty?
|
|
38
|
+
|
|
39
|
+
obfuscated = js_code.dup
|
|
40
|
+
|
|
41
|
+
# Layer 1: Remove comments and extra whitespace
|
|
42
|
+
obfuscated = remove_comments_and_whitespace(obfuscated)
|
|
43
|
+
|
|
44
|
+
# Layer 2: Safe variable renaming (EXCLUDE neonPulse)
|
|
45
|
+
obfuscated = rename_variables(obfuscated)
|
|
46
|
+
|
|
47
|
+
# Layer 3: String obfuscation
|
|
48
|
+
obfuscated = obfuscate_strings(obfuscated)
|
|
49
|
+
|
|
50
|
+
# Layer 4: Number obfuscation
|
|
51
|
+
obfuscated = obfuscate_numbers(obfuscated)
|
|
52
|
+
|
|
53
|
+
# Layer 5: Split into chunks and reassemble
|
|
54
|
+
final_js = split_and_reassemble(obfuscated, parts: 6)
|
|
55
|
+
|
|
56
|
+
final_js
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def remove_comments_and_whitespace(code)
|
|
62
|
+
# Remove block comments
|
|
63
|
+
code.gsub!(/\/\*[\s\S]*?\*\//, '')
|
|
64
|
+
# Remove line comments
|
|
65
|
+
code.gsub!(/\/\/[^\n\r]*/, '')
|
|
66
|
+
# Collapse multiple whitespace
|
|
67
|
+
code.gsub!(/\s+/, ' ')
|
|
68
|
+
# Remove spaces around operators
|
|
69
|
+
code.gsub!(/\s*([=+\-*\/&|^!<>?{}();:,])\s*/, '\1')
|
|
70
|
+
# Remove multiple semicolons
|
|
71
|
+
code.gsub!(/;\s*;/, ';')
|
|
72
|
+
code.strip
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def rename_variables(code)
|
|
76
|
+
# Safe renaming - EXCLUDE neonPulse from renaming
|
|
77
|
+
renames = {
|
|
78
|
+
'NeonPulse' => 'NP', # Rename class internally
|
|
79
|
+
# 'neonPulse' => 'np', # DON'T rename the global instance
|
|
80
|
+
'signalName' => 'sN',
|
|
81
|
+
'initialValue' => 'iV',
|
|
82
|
+
'element' => 'el',
|
|
83
|
+
'property' => 'pr',
|
|
84
|
+
'formData' => 'fD',
|
|
85
|
+
'callback' => 'cb',
|
|
86
|
+
'handler' => 'hd',
|
|
87
|
+
'processor' => 'pc',
|
|
88
|
+
'action' => 'ac',
|
|
89
|
+
'event' => 'ev'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
renames.each do |original, replacement|
|
|
93
|
+
code.gsub!(/\b#{original}\b/, replacement)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
code
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def obfuscate_strings(code)
|
|
100
|
+
# Obfuscate double-quoted strings
|
|
101
|
+
code.gsub!(/"([^"\\]*(\\.[^"\\]*)*)"/) do |match|
|
|
102
|
+
content = $1
|
|
103
|
+
# Mix of hex and unicode escapes
|
|
104
|
+
obfuscated_content = content.chars.map.with_index { |c, i|
|
|
105
|
+
if i % 3 == 0
|
|
106
|
+
"\\x#{c.ord.to_s(16)}"
|
|
107
|
+
elsif i % 3 == 1
|
|
108
|
+
"\\u#{c.ord.to_s(16).rjust(4, '0')}"
|
|
109
|
+
else
|
|
110
|
+
c
|
|
111
|
+
end
|
|
112
|
+
}.join
|
|
113
|
+
"\"#{obfuscated_content}\""
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
code
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def obfuscate_numbers(code)
|
|
120
|
+
# Convert some numbers to expressions
|
|
121
|
+
code.gsub!(/\b(\d+)\b/) do |match|
|
|
122
|
+
num = $1.to_i
|
|
123
|
+
if num > 5 && num < 100
|
|
124
|
+
case rand(4)
|
|
125
|
+
when 0 then "(#{num - 1} + 1)"
|
|
126
|
+
when 1 then "(#{num * 2} / 2)"
|
|
127
|
+
when 2 then "(#{num} * 1)"
|
|
128
|
+
when 3 then "0x#{num.to_s(16)}"
|
|
129
|
+
else match
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
match
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
code
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# lib/hyraft/compiler/parser.rb
|
|
2
|
+
module Hyraft
|
|
3
|
+
module Compiler
|
|
4
|
+
class HyraftParser
|
|
5
|
+
def initialize(template)
|
|
6
|
+
@template = template
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def parse
|
|
10
|
+
{
|
|
11
|
+
metadata: extract('metadata','html'),
|
|
12
|
+
metas: extract('metas','html'),
|
|
13
|
+
displayer: extract('displayer','html'),
|
|
14
|
+
transmuter: extract('transmuter','rb'),
|
|
15
|
+
manifestor: extract('manifestor','js'),
|
|
16
|
+
styles: @template.scan(/<style\s+src="([^"]+)"\s*\/?>/).flatten
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def extract(tag, lang)
|
|
23
|
+
@template[/<#{tag}\s+#{lang}>(.*?)<\/#{tag}>/m, 1]&.strip
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# lib/hyraft/compiler/renderer.rb
|
|
2
|
+
require_relative 'javascript_library'
|
|
3
|
+
require_relative 'html_purifier'
|
|
4
|
+
|
|
5
|
+
module Hyraft
|
|
6
|
+
module Compiler
|
|
7
|
+
class HyraftRenderer
|
|
8
|
+
include HtmlPurifier
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def initialize(obfuscation_method: :multi_layer)
|
|
12
|
+
@obfuscation_method = obfuscation_method
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render(layout, parsed, locals = {})
|
|
16
|
+
locals&.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
17
|
+
|
|
18
|
+
# Initialize required JS storage
|
|
19
|
+
@required_js = []
|
|
20
|
+
|
|
21
|
+
# Process requires from metadata FIRST
|
|
22
|
+
if parsed[:metadata]
|
|
23
|
+
metadata_content = parsed[:metadata].to_s
|
|
24
|
+
|
|
25
|
+
# Extract and process requires
|
|
26
|
+
requires = extract_requires(metadata_content)
|
|
27
|
+
requires.each { |file_path| load_required_file(file_path) }
|
|
28
|
+
# Remove requires from metadata for final rendering
|
|
29
|
+
parsed[:metadata] = remove_requires(metadata_content)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if parsed[:transmuter]
|
|
33
|
+
transmuter_code = parsed[:transmuter]
|
|
34
|
+
processed_transmuter = convert_html_tags(transmuter_code)
|
|
35
|
+
instance_eval(processed_transmuter)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
content = render_displayer(parsed[:displayer].to_s)
|
|
39
|
+
styles = (parsed[:styles] || []).map { |s| "<link rel='stylesheet' href='#{s}'>" }.join
|
|
40
|
+
|
|
41
|
+
# Combine required JS with main manifestor - IN CORRECT ORDER
|
|
42
|
+
all_js = []
|
|
43
|
+
all_js += @required_js if @required_js # Required libraries first
|
|
44
|
+
|
|
45
|
+
# Add main manifestor code (no obfuscation for app code by default)
|
|
46
|
+
if parsed[:manifestor] && !parsed[:manifestor].strip.empty?
|
|
47
|
+
all_js << parsed[:manifestor]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Combine all JavaScript
|
|
51
|
+
js_content = all_js.join("\n")
|
|
52
|
+
js = js_content.strip.empty? ? '' : "<script>#{js_content}</script>"
|
|
53
|
+
|
|
54
|
+
metas = render_displayer(parsed[:metadata].to_s)
|
|
55
|
+
|
|
56
|
+
result = layout.dup
|
|
57
|
+
inject_title(result, find_title(metas))
|
|
58
|
+
inject_metas(result, metas)
|
|
59
|
+
result.gsub!('<hyraft styles="css">', styles)
|
|
60
|
+
.gsub!('<hyraft content="hyraft">', content)
|
|
61
|
+
.gsub!('<hyraft script="javascript">', js)
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def extract_requires(metadata_content)
|
|
74
|
+
requires = []
|
|
75
|
+
metadata_content.scan(/<require\s+file="([^"]+)"\s*\/>/) do |match|
|
|
76
|
+
requires << match.first
|
|
77
|
+
end
|
|
78
|
+
requires
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def remove_requires(metadata_content)
|
|
82
|
+
metadata_content.gsub(/<require\s+file="[^"]+"\s*\/>/, '')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def load_required_file(relative_path)
|
|
86
|
+
# Clean the library name (remove .hyr extension if present)
|
|
87
|
+
clean_name = relative_path.gsub(/\.hyr$/, '')
|
|
88
|
+
|
|
89
|
+
# Get the obfuscated library code
|
|
90
|
+
js_code = JavaScriptLibrary.get(clean_name, obfuscation_method: @obfuscation_method)
|
|
91
|
+
|
|
92
|
+
if js_code
|
|
93
|
+
@required_js ||= []
|
|
94
|
+
@required_js << js_code
|
|
95
|
+
else
|
|
96
|
+
# If not found in JavaScriptLibrary, fall back to file system search
|
|
97
|
+
full_path = find_required_file_anywhere(relative_path)
|
|
98
|
+
|
|
99
|
+
if full_path && File.exist?(full_path)
|
|
100
|
+
content = File.read(full_path)
|
|
101
|
+
parsed_required = parse_content(content)
|
|
102
|
+
|
|
103
|
+
# Process transmuter from required files (for Ruby code)
|
|
104
|
+
if parsed_required[:transmuter] && !parsed_required[:transmuter].strip.empty?
|
|
105
|
+
required_transmuter = convert_html_tags(parsed_required[:transmuter])
|
|
106
|
+
instance_eval(required_transmuter)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Store manifestor content from required files (for JavaScript)
|
|
110
|
+
if parsed_required[:manifestor] && !parsed_required[:manifestor].strip.empty?
|
|
111
|
+
@required_js ||= []
|
|
112
|
+
# Apply obfuscation to file-based libraries too
|
|
113
|
+
file_js = parsed_required[:manifestor]
|
|
114
|
+
obfuscated_file_js = case @obfuscation_method
|
|
115
|
+
when :split_and_reassemble
|
|
116
|
+
JavaScriptObfuscator.split_and_reassemble(file_js)
|
|
117
|
+
when :multi_layer
|
|
118
|
+
JavaScriptObfuscator.multi_layer_obfuscation(file_js)
|
|
119
|
+
else
|
|
120
|
+
file_js
|
|
121
|
+
end
|
|
122
|
+
@required_js << obfuscated_file_js
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
puts "WARNING: Required file not found: #{relative_path}"
|
|
126
|
+
puts "Available built-in libraries: #{JavaScriptLibrary.available_libraries.join(', ')}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def find_required_file_anywhere(relative_path)
|
|
132
|
+
# Remove .hyr extension if present
|
|
133
|
+
clean_path = relative_path.gsub(/\.hyr$/, '')
|
|
134
|
+
|
|
135
|
+
# Search pattern: adapter-intake/*/display/**/{relative_path}.hyr
|
|
136
|
+
search_pattern = File.join(ROOT, 'adapter-intake', '*', 'display', '**', "#{clean_path}.hyr")
|
|
137
|
+
matching_files = Dir.glob(search_pattern)
|
|
138
|
+
|
|
139
|
+
matching_files.first # Return first match
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def parse_content(content)
|
|
143
|
+
sections = {
|
|
144
|
+
metadata: content[/<metadata[^>]*>(.*?)<\/metadata>/m, 1],
|
|
145
|
+
displayer: content[/<displayer[^>]*>(.*?)<\/displayer>/m, 1],
|
|
146
|
+
transmuter: content[/<transmuter[^>]*>(.*?)<\/transmuter>/m, 1],
|
|
147
|
+
manifestor: content[/<manifestor[^>]*>(.*?)<\/manifestor>/m, 1],
|
|
148
|
+
styles: content.scan(/<style\s+src="([^"]+)"\s*\/?>/).flatten
|
|
149
|
+
}
|
|
150
|
+
sections
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def convert_html_tags(code)
|
|
157
|
+
code.gsub(/<html>([\s\S]*?)<\/html>/) do |match|
|
|
158
|
+
content = $1.strip
|
|
159
|
+
|
|
160
|
+
lines = content.split("\n")
|
|
161
|
+
indented_content = lines.map { |line| " #{line}" }.join("\n")
|
|
162
|
+
"<<~HTML\n#{indented_content}\nHTML"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def render_displayer(html)
|
|
167
|
+
html.gsub(/\[\.\s*(\w+)\s*\.\]/) { respond_to?($1) ? send($1).to_s : "[.#{$1}.]" }
|
|
168
|
+
.gsub(/<([\w-]+)(\s+[^>]*)?\s*\/>/) do |match|
|
|
169
|
+
tag = $1
|
|
170
|
+
attr_str = $2
|
|
171
|
+
render_component(tag, attr_str)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def render_component(tag, attr_str)
|
|
176
|
+
method_name = "display_#{tag.tr('-', '_')}"
|
|
177
|
+
attrs = parse_attributes(attr_str.to_s)
|
|
178
|
+
|
|
179
|
+
if respond_to?(method_name)
|
|
180
|
+
return send(method_name, **attrs)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
attr_html = attrs.map { |k,v| "#{k}='#{v}'" }.join(' ')
|
|
184
|
+
"<div class='#{tag}' #{attr_html}>#{attrs[:name] || tag}</div>"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def parse_attributes(str)
|
|
188
|
+
return {} if str.nil? || str.empty?
|
|
189
|
+
str.scan(/(\w+)="(.*?)"/).to_h { |k,v| [k.to_sym, v] }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def find_title(metas)
|
|
193
|
+
metas[/\<title\>(.*?)\<\/title\>/m, 1] ||
|
|
194
|
+
(instance_variable_defined?(:@page_title) && @page_title) ||
|
|
195
|
+
(page_title if respond_to?(:page_title))
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def inject_title(result, title)
|
|
199
|
+
return unless title
|
|
200
|
+
result.sub!(/<title>.*<\/title>/m, "<title>#{title}</title>") ||
|
|
201
|
+
result.sub!('</head>', " <title>#{title}</title>\n</head>")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def inject_metas(result, metas)
|
|
205
|
+
return if metas.empty?
|
|
206
|
+
|
|
207
|
+
if result.include?('<hyraft meta="tags">')
|
|
208
|
+
result.gsub!('<hyraft meta="tags">', metas)
|
|
209
|
+
elsif result.include?('<hyraft styles="css">')
|
|
210
|
+
result.sub!('<hyraft styles="css">', "#{metas}\n<hyraft styles=\"css\">")
|
|
211
|
+
else
|
|
212
|
+
result.sub!('</head>', "#{metas}\n</head>")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hyraft
|
|
4
|
+
class Circuit
|
|
5
|
+
def initialize(ports = {})
|
|
6
|
+
@ports = ports
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Magic: Automatically route custom methods to execute
|
|
10
|
+
def method_missing(method, *args, &block)
|
|
11
|
+
if respond_to_missing?(method)
|
|
12
|
+
execute(operation: method, params: args.first || {})
|
|
13
|
+
else
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def respond_to_missing?(method, include_private = false)
|
|
19
|
+
# Check if it's a public method defined in the subclass OR
|
|
20
|
+
# if the execute method can handle it
|
|
21
|
+
self.class.public_method_defined?(method) || can_handle_operation?(method)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute(input = {})
|
|
25
|
+
raise NotImplementedError, "Circuits must implement execute method"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def can_handle_operation?(operation)
|
|
31
|
+
# Subclasses should override this to return true for operations they handle
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|