@ylsoo/core 1.1.0 → 2.0.1
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.
- package/README.md +50 -67
- package/index.d.ts +100 -49
- package/package.json +12 -8
- package/src/core/analytics.js +66 -0
- package/src/core/cache.js +31 -0
- package/src/core/i18n.js +42 -0
- package/src/core/logger.js +39 -0
- package/src/core/state.js +67 -0
- package/src/core/storage.js +99 -0
- package/src/engine/config.js +54 -0
- package/src/engine/features.js +59 -0
- package/src/events/bus.js +69 -0
- package/src/http/client.js +49 -0
- package/src/index.js +68 -0
- package/src/resilience/breaker.js +73 -0
- package/src/router/domRouter.js +131 -0
- package/src/security/crypto.js +67 -0
- package/src/utils/helpers.js +43 -0
- package/src/utils/queue.js +50 -0
- package/src/utils/time.js +46 -0
- package/src/validation/schema.js +53 -0
- package/index.js +0 -202
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
class YlsooStorage {
|
|
2
|
+
constructor(cryptoEngine) {
|
|
3
|
+
this.crypto = cryptoEngine; // Instance of YlsooCrypto
|
|
4
|
+
this.vaultPath = '.ylsoo-vault.json';
|
|
5
|
+
|
|
6
|
+
// Check environment (distinguish Node from Browser safely)
|
|
7
|
+
this.isBrowser = typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Very rudimentary AES-like masking to keep it zero-dependency and cross-platform
|
|
11
|
+
// without heavily relying on async SubtleCrypto which makes synchronous read/writes impossible here.
|
|
12
|
+
// We use Base64 padding for simplistic obfuscation here to keep the API synchronous.
|
|
13
|
+
// For true AES, it would require async/await API changes.
|
|
14
|
+
_obfuscate(str) {
|
|
15
|
+
return this.crypto.encodeBase64(str).split('').reverse().join('');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_deobfuscate(obf) {
|
|
19
|
+
const b64 = obf.split('').reverse().join('');
|
|
20
|
+
return this.crypto.decodeBase64(b64);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sets a value securely
|
|
25
|
+
*/
|
|
26
|
+
set(key, value) {
|
|
27
|
+
const raw = JSON.stringify(value);
|
|
28
|
+
const secureString = this._obfuscate(raw);
|
|
29
|
+
const secureKey = this._obfuscate(key);
|
|
30
|
+
|
|
31
|
+
if (this.isBrowser) {
|
|
32
|
+
globalThis.localStorage.setItem(secureKey, secureString);
|
|
33
|
+
} else {
|
|
34
|
+
this._writeNodeFile(secureKey, secureString);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Retrieves a secure value
|
|
40
|
+
*/
|
|
41
|
+
get(key) {
|
|
42
|
+
const secureKey = this._obfuscate(key);
|
|
43
|
+
let secureString = null;
|
|
44
|
+
|
|
45
|
+
if (this.isBrowser) {
|
|
46
|
+
secureString = globalThis.localStorage.getItem(secureKey);
|
|
47
|
+
} else {
|
|
48
|
+
secureString = this._readNodeFile(secureKey);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!secureString) return null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const raw = this._deobfuscate(secureString);
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
} catch(e) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Removes a secure key
|
|
63
|
+
*/
|
|
64
|
+
remove(key) {
|
|
65
|
+
const secureKey = this._obfuscate(key);
|
|
66
|
+
if (this.isBrowser) {
|
|
67
|
+
globalThis.localStorage.removeItem(secureKey);
|
|
68
|
+
} else {
|
|
69
|
+
const map = this._getNodeMap();
|
|
70
|
+
delete map[secureKey];
|
|
71
|
+
require('fs').writeFileSync(this.vaultPath, JSON.stringify(map), 'utf8');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -- Node.js Fallbacks --
|
|
76
|
+
_getNodeMap() {
|
|
77
|
+
try {
|
|
78
|
+
const fs = require('fs');
|
|
79
|
+
if (!fs.existsSync(this.vaultPath)) return {};
|
|
80
|
+
const raw = fs.readFileSync(this.vaultPath, 'utf8');
|
|
81
|
+
return JSON.parse(raw);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_writeNodeFile(key, val) {
|
|
88
|
+
const map = this._getNodeMap();
|
|
89
|
+
map[key] = val;
|
|
90
|
+
require('fs').writeFileSync(this.vaultPath, JSON.stringify(map), 'utf8');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_readNodeFile(key) {
|
|
94
|
+
const map = this._getNodeMap();
|
|
95
|
+
return map[key] || null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { YlsooStorage };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
class YlsooConfig {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.store = {};
|
|
4
|
+
this.isFrozen = false;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Defines a base configuration
|
|
9
|
+
* @param {object} defaults
|
|
10
|
+
*/
|
|
11
|
+
setDefault(defaults) {
|
|
12
|
+
if (this.isFrozen) throw new Error('[Ylsoo Config] Cannot mutate after freezing.');
|
|
13
|
+
this.store = { ...defaults };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Deep merges environment overrides over defaults
|
|
18
|
+
* @param {object} envOverrides
|
|
19
|
+
*/
|
|
20
|
+
applyEnvironment(envOverrides) {
|
|
21
|
+
if (this.isFrozen) throw new Error('[Ylsoo Config] Cannot mutate after freezing.');
|
|
22
|
+
this.store = this._deepMerge(this.store, envOverrides);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Locks the config so it cannot be altered during application runtime securely.
|
|
27
|
+
*/
|
|
28
|
+
freeze() {
|
|
29
|
+
this.isFrozen = true;
|
|
30
|
+
Object.freeze(this.store);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fetches the entire config or a specific key
|
|
35
|
+
* @param {string} key Optional
|
|
36
|
+
*/
|
|
37
|
+
get(key = null) {
|
|
38
|
+
if (key) return this.store[key];
|
|
39
|
+
return this.store;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Purely native deep merge logic
|
|
43
|
+
_deepMerge(target, source) {
|
|
44
|
+
for (const key of Object.keys(source)) {
|
|
45
|
+
if (source[key] instanceof Object && key in target) {
|
|
46
|
+
Object.assign(source[key], this._deepMerge(target[key], source[key]));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
Object.assign(target || {}, source);
|
|
50
|
+
return target;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { YlsooConfig };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
class YlsooFeatureFlags {
|
|
2
|
+
constructor(cryptoEngine) {
|
|
3
|
+
this.crypto = cryptoEngine; // Need crypto to hash unique IDs for deterministic A/B testing
|
|
4
|
+
this.flags = {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register a feature flag ruleset
|
|
9
|
+
* @param {string} flagName
|
|
10
|
+
* @param {object} rules { enabled: boolean, rolloutPercentage: number, conditions: object }
|
|
11
|
+
*/
|
|
12
|
+
setFlag(flagName, rules) {
|
|
13
|
+
this.flags[flagName] = {
|
|
14
|
+
enabled: false,
|
|
15
|
+
rolloutPercentage: 100, // 0 to 100
|
|
16
|
+
conditions: {}, // e.g. { country: 'US', tier: 'pro' }
|
|
17
|
+
...rules
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Evaluate if a feature is enabled for a specific context natively
|
|
23
|
+
* @param {string} flagName
|
|
24
|
+
* @param {object} context { userId: string, attributes: { country: 'US', tier: 'pro' }}
|
|
25
|
+
*/
|
|
26
|
+
async evaluate(flagName, context = {}) {
|
|
27
|
+
const flag = this.flags[flagName];
|
|
28
|
+
if (!flag) return false;
|
|
29
|
+
|
|
30
|
+
// 1. Master Kill Switch check
|
|
31
|
+
if (flag.enabled === false) return false;
|
|
32
|
+
|
|
33
|
+
// 2. Fractional Rollout Evaluation
|
|
34
|
+
if (flag.rolloutPercentage < 100) {
|
|
35
|
+
if (!context.userId) return false; // Needs persistent ID for A/B rollout tracking
|
|
36
|
+
|
|
37
|
+
// We hash the flagName + userId so the same user ALWAYS gets the same result for a specific flag consistently.
|
|
38
|
+
const hashStr = await this.crypto.hash(`${flagName}-${context.userId}`);
|
|
39
|
+
// Natively convert first 8 chars of hex hash into an integer, modulus 100 to get a 0-99 distribution
|
|
40
|
+
const bucket = parseInt(hashStr.substring(0, 8), 16) % 100;
|
|
41
|
+
|
|
42
|
+
if (bucket >= flag.rolloutPercentage) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Context Targeting Rules
|
|
48
|
+
for (const [key, expectedValue] of Object.entries(flag.conditions)) {
|
|
49
|
+
const userValue = (context.attributes || {})[key];
|
|
50
|
+
if (userValue !== expectedValue) {
|
|
51
|
+
return false; // Missed targeting matrix
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { YlsooFeatureFlags };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
class YlsooEventBus {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.events = {};
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Subscribe to an event
|
|
8
|
+
* @param {string} event
|
|
9
|
+
* @param {Function} callback
|
|
10
|
+
* @returns {Function} Unsubscribe function
|
|
11
|
+
*/
|
|
12
|
+
on(event, callback) {
|
|
13
|
+
if (!this.events[event]) {
|
|
14
|
+
this.events[event] = [];
|
|
15
|
+
}
|
|
16
|
+
this.events[event].push(callback);
|
|
17
|
+
|
|
18
|
+
// Return an un-subscriber
|
|
19
|
+
return () => this.off(event, callback);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Unsubscribe from an event
|
|
24
|
+
* @param {string} event
|
|
25
|
+
* @param {Function} callback
|
|
26
|
+
*/
|
|
27
|
+
off(event, callback) {
|
|
28
|
+
if (!this.events[event]) return;
|
|
29
|
+
this.events[event] = this.events[event].filter(cb => cb !== callback);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Publish an event with optional payload
|
|
34
|
+
* @param {string} event
|
|
35
|
+
* @param {*} data
|
|
36
|
+
*/
|
|
37
|
+
emit(event, data) {
|
|
38
|
+
if (!this.events[event]) return;
|
|
39
|
+
this.events[event].forEach(callback => {
|
|
40
|
+
try {
|
|
41
|
+
callback(data);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(`[Ylsoo Event Bus] Error in callback for event "${event}":`, err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Listen for an event exactly once
|
|
50
|
+
* @param {string} event
|
|
51
|
+
* @param {Function} callback
|
|
52
|
+
*/
|
|
53
|
+
once(event, callback) {
|
|
54
|
+
const wrapper = (data) => {
|
|
55
|
+
this.off(event, wrapper);
|
|
56
|
+
callback(data);
|
|
57
|
+
};
|
|
58
|
+
this.on(event, wrapper);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear all events
|
|
63
|
+
*/
|
|
64
|
+
clear() {
|
|
65
|
+
this.events = {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { YlsooEventBus };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class YlsooHttp {
|
|
2
|
+
async request(endpoint, options = {}) {
|
|
3
|
+
const defaultHeaders = {
|
|
4
|
+
'Content-Type': 'application/json',
|
|
5
|
+
'User-Agent': 'Ylsoo-Core/2.0'
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const config = {
|
|
9
|
+
...options,
|
|
10
|
+
headers: {
|
|
11
|
+
...defaultHeaders,
|
|
12
|
+
...options.headers
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (config.body && typeof config.body === 'object') {
|
|
17
|
+
config.body = JSON.stringify(config.body);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(endpoint, config);
|
|
22
|
+
const isJson = response.headers.get('content-type')?.includes('application/json');
|
|
23
|
+
|
|
24
|
+
const data = isJson ? await response.json() : await response.text();
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw {
|
|
28
|
+
status: response.status,
|
|
29
|
+
message: response.statusText,
|
|
30
|
+
data
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return data;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(`[Ylsoo HTTP Error]: ${error.message || JSON.stringify(error)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get(endpoint, headers = {}) {
|
|
41
|
+
return this.request(endpoint, { method: 'GET', headers });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
post(endpoint, body, headers = {}) {
|
|
45
|
+
return this.request(endpoint, { method: 'POST', body, headers });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { YlsooHttp };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ylsoo/core v2.2.0
|
|
3
|
+
* Enterprise utilities for both Node.js and the Web.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { YlsooHttp } = require('./http/client');
|
|
7
|
+
const { YlsooLogger } = require('./core/logger');
|
|
8
|
+
const { YlsooCache } = require('./core/cache');
|
|
9
|
+
const { YlsooCrypto } = require('./security/crypto');
|
|
10
|
+
const { YlsooEventBus } = require('./events/bus');
|
|
11
|
+
const { YlsooHelpers } = require('./utils/helpers');
|
|
12
|
+
const { YlsooState } = require('./core/state');
|
|
13
|
+
const { YlsooAnalytics } = require('./core/analytics');
|
|
14
|
+
const { Ylsoo18n } = require('./core/i18n');
|
|
15
|
+
const { YlsooStorage } = require('./core/storage');
|
|
16
|
+
const { YlsooValidator } = require('./validation/schema');
|
|
17
|
+
|
|
18
|
+
// V2.2.0 Ultra-Advanced Modules
|
|
19
|
+
const { YlsooRouter } = require('./router/domRouter');
|
|
20
|
+
const { YlsooFeatureFlags } = require('./engine/features');
|
|
21
|
+
const { YlsooResilience } = require('./resilience/breaker');
|
|
22
|
+
const { YlsooTaskQueue } = require('./utils/queue');
|
|
23
|
+
const { YlsooTime } = require('./utils/time');
|
|
24
|
+
const { YlsooConfig } = require('./engine/config');
|
|
25
|
+
|
|
26
|
+
class YlsooCore {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.version = '2.2.0';
|
|
29
|
+
|
|
30
|
+
// Engine & Structure
|
|
31
|
+
this.crypto = new YlsooCrypto();
|
|
32
|
+
this.config = new YlsooConfig();
|
|
33
|
+
this.router = new YlsooRouter();
|
|
34
|
+
this.feature = new YlsooFeatureFlags(this.crypto);
|
|
35
|
+
|
|
36
|
+
// Memory & Data
|
|
37
|
+
this.state = new YlsooState();
|
|
38
|
+
this.cache = new YlsooCache();
|
|
39
|
+
this.storage = new YlsooStorage(this.crypto);
|
|
40
|
+
this.validator = new YlsooValidator();
|
|
41
|
+
|
|
42
|
+
// Networking & Flow
|
|
43
|
+
this.http = new YlsooHttp();
|
|
44
|
+
this.events = new YlsooEventBus();
|
|
45
|
+
this.resilience = new YlsooResilience();
|
|
46
|
+
this.analytics = new YlsooAnalytics(this.http);
|
|
47
|
+
|
|
48
|
+
// Helpers
|
|
49
|
+
this.logger = new YlsooLogger();
|
|
50
|
+
this.time = new YlsooTime();
|
|
51
|
+
this.i18n = new Ylsoo18n();
|
|
52
|
+
this.helpers = new YlsooHelpers();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Helper Proxies to keep backwards compatibility ---
|
|
56
|
+
sleep(ms) { return this.helpers.sleep(ms); }
|
|
57
|
+
deepClone(obj) { return this.helpers.deepClone(obj); }
|
|
58
|
+
debounce(func, wait) { return this.helpers.debounce(func, wait); }
|
|
59
|
+
isEmpty(val) { return this.helpers.isEmpty(val); }
|
|
60
|
+
capitalize(str) { return this.helpers.capitalize(str); }
|
|
61
|
+
|
|
62
|
+
validate(data, schema) { return this.validator.validate(data, schema); }
|
|
63
|
+
|
|
64
|
+
// Expose the raw Queue Class so developers can instantiate multiple queues if needed
|
|
65
|
+
createQueue(limit = 5) { return new YlsooTaskQueue(limit); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = new YlsooCore();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
class YlsooResilience {
|
|
2
|
+
/**
|
|
3
|
+
* Exponential backoff retry wrapper
|
|
4
|
+
* @param {Function} asyncFunction
|
|
5
|
+
* @param {number} maxRetries
|
|
6
|
+
* @param {number} baseDelayMs
|
|
7
|
+
*/
|
|
8
|
+
async withRetry(asyncFunction, maxRetries = 3, baseDelayMs = 1000) {
|
|
9
|
+
let attempt = 0;
|
|
10
|
+
while (attempt <= maxRetries) {
|
|
11
|
+
try {
|
|
12
|
+
return await asyncFunction();
|
|
13
|
+
} catch (err) {
|
|
14
|
+
attempt++;
|
|
15
|
+
if (attempt > maxRetries) {
|
|
16
|
+
throw new Error(`[Ylsoo Resilience] Failed after ${maxRetries} retries. Final Error: ${err.message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Exponential backoff logic: 1s, 2s, 4s...
|
|
20
|
+
const delay = baseDelayMs * Math.pow(2, attempt - 1);
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a Circuit Breaker wrapper to prevent blasting dead APIs
|
|
28
|
+
* @param {Function} asyncFunction
|
|
29
|
+
* @param {object} options
|
|
30
|
+
*/
|
|
31
|
+
createBreaker(asyncFunction, options = {}) {
|
|
32
|
+
const config = {
|
|
33
|
+
failureThreshold: 5, // Fail 5 times before breaking
|
|
34
|
+
resetTimeoutMs: 30000, // Keep broken for 30s before testing again
|
|
35
|
+
...options
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let failures = 0;
|
|
39
|
+
let state = 'CLOSED'; // CLOSED (working), OPEN (broken API), HALF_OPEN (testing)
|
|
40
|
+
let nextAttemptTimestamp = 0;
|
|
41
|
+
|
|
42
|
+
return async (...args) => {
|
|
43
|
+
if (state === 'OPEN') {
|
|
44
|
+
if (Date.now() >= nextAttemptTimestamp) {
|
|
45
|
+
state = 'HALF_OPEN';
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error('[Ylsoo Circuit Breaker] Circuit is OPEN. Request dropped to protect upstream service.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = await asyncFunction(...args);
|
|
53
|
+
|
|
54
|
+
// Reset breaker on success
|
|
55
|
+
if (state === 'HALF_OPEN') {
|
|
56
|
+
state = 'CLOSED';
|
|
57
|
+
failures = 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
failures++;
|
|
63
|
+
if (failures >= config.failureThreshold || state === 'HALF_OPEN') {
|
|
64
|
+
state = 'OPEN';
|
|
65
|
+
nextAttemptTimestamp = Date.now() + config.resetTimeoutMs;
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { YlsooResilience };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
class YlsooRouter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.routes = [];
|
|
4
|
+
this.middlewares = [];
|
|
5
|
+
this.currentRoute = null;
|
|
6
|
+
this.fallback = null;
|
|
7
|
+
this.isBrowser = typeof window !== 'undefined' && typeof window.history !== 'undefined';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Adds a global middleware hook
|
|
12
|
+
* @param {Function} hook (to, from, next)
|
|
13
|
+
*/
|
|
14
|
+
beforeEach(hook) {
|
|
15
|
+
this.middlewares.push(hook);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a route mapping
|
|
20
|
+
* @param {string} path (e.g. "/users/:id")
|
|
21
|
+
* @param {Function} handler
|
|
22
|
+
*/
|
|
23
|
+
add(path, handler) {
|
|
24
|
+
// Generate Regex for path parsing
|
|
25
|
+
const paramNames = [];
|
|
26
|
+
let regexPath = path.replace(/([:*])(\w+)/g, (full, colon, name) => {
|
|
27
|
+
paramNames.push(name);
|
|
28
|
+
return '([^/]+)';
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Allow wildcards
|
|
32
|
+
regexPath = regexPath.replace(/\*/g, '.*');
|
|
33
|
+
|
|
34
|
+
this.routes.push({
|
|
35
|
+
path,
|
|
36
|
+
regex: new RegExp(`^${regexPath}/?$`, 'i'),
|
|
37
|
+
paramNames,
|
|
38
|
+
handler
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set a 404 Not Found fallback route
|
|
44
|
+
* @param {Function} handler
|
|
45
|
+
*/
|
|
46
|
+
setFallback(handler) {
|
|
47
|
+
this.fallback = handler;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Start the router engine, bind to HTML5 History and PopState
|
|
52
|
+
*/
|
|
53
|
+
start() {
|
|
54
|
+
if (!this.isBrowser) return; // Silent fail in Node environments
|
|
55
|
+
|
|
56
|
+
// Bind back/forward buttons
|
|
57
|
+
window.addEventListener('popstate', () => {
|
|
58
|
+
this._navigate(window.location.pathname, false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Intercept clicks
|
|
62
|
+
document.body.addEventListener('click', (e) => {
|
|
63
|
+
const target = e.target.closest('a');
|
|
64
|
+
if (target && target.hasAttribute('data-route')) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
this._navigate(target.getAttribute('href'));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Run initial parse
|
|
71
|
+
this._navigate(window.location.pathname, false);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Navigate to a route manually
|
|
76
|
+
* @param {string} path
|
|
77
|
+
*/
|
|
78
|
+
push(path) {
|
|
79
|
+
if (!this.isBrowser) return;
|
|
80
|
+
this._navigate(path, true);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async _navigate(urlPath, pushToHistory = true) {
|
|
84
|
+
const fromPath = this.currentRoute;
|
|
85
|
+
|
|
86
|
+
// Run middlewares
|
|
87
|
+
let halt = false;
|
|
88
|
+
const next = (confirm = true) => { if (confirm === false) halt = true; };
|
|
89
|
+
|
|
90
|
+
for (let mw of this.middlewares) {
|
|
91
|
+
await mw(urlPath, fromPath, next);
|
|
92
|
+
if (halt) return; // Middleware blocked navigation
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Match route
|
|
96
|
+
let matchedRoute = null;
|
|
97
|
+
let params = {};
|
|
98
|
+
|
|
99
|
+
for (let route of this.routes) {
|
|
100
|
+
const match = urlPath.match(route.regex);
|
|
101
|
+
if (match) {
|
|
102
|
+
matchedRoute = route;
|
|
103
|
+
route.paramNames.forEach((name, idx) => {
|
|
104
|
+
params[name] = match[idx + 1];
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (pushToHistory && urlPath !== window.location.pathname) {
|
|
111
|
+
window.history.pushState(null, '', urlPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.currentRoute = urlPath;
|
|
115
|
+
|
|
116
|
+
// Parse Query Params (Only in browser)
|
|
117
|
+
let query = {};
|
|
118
|
+
if (this.isBrowser && typeof window !== 'undefined') {
|
|
119
|
+
const url = new URL(window.location.href);
|
|
120
|
+
query = Object.fromEntries(url.searchParams.entries());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (matchedRoute) {
|
|
124
|
+
matchedRoute.handler({ params, query, path: urlPath });
|
|
125
|
+
} else if (this.fallback) {
|
|
126
|
+
this.fallback({ path: urlPath, query });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { YlsooRouter };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class YlsooCrypto {
|
|
2
|
+
/**
|
|
3
|
+
* Base64 encode a string
|
|
4
|
+
* @param {string} str
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
encodeBase64(str) {
|
|
8
|
+
if (typeof globalThis.btoa === 'function') {
|
|
9
|
+
return globalThis.btoa(str);
|
|
10
|
+
}
|
|
11
|
+
return Buffer.from(str).toString('base64');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base64 decode a string
|
|
16
|
+
* @param {string} base64
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
decodeBase64(base64) {
|
|
20
|
+
if (typeof globalThis.atob === 'function') {
|
|
21
|
+
return globalThis.atob(base64);
|
|
22
|
+
}
|
|
23
|
+
return Buffer.from(base64, 'base64').toString('utf-8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a SHA-256 hash of a string natively
|
|
28
|
+
* @param {string} str
|
|
29
|
+
* @returns {Promise<string>}
|
|
30
|
+
*/
|
|
31
|
+
async hash(str) {
|
|
32
|
+
// Attempt standard Web Crypto API (Browser + Modern Node)
|
|
33
|
+
if (globalThis.crypto && globalThis.crypto.subtle) {
|
|
34
|
+
const msgBuffer = new TextEncoder().encode(str);
|
|
35
|
+
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', msgBuffer);
|
|
36
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
37
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback for older Node.js contexts
|
|
41
|
+
try {
|
|
42
|
+
const crypto = require('crypto');
|
|
43
|
+
return crypto.createHash('sha256').update(str).digest('hex');
|
|
44
|
+
} catch(err) {
|
|
45
|
+
throw new Error('[Ylsoo Crypto Error] No secure hashing engine available on this platform.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate a secure random v4 UUID
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
uuid() {
|
|
54
|
+
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
|
|
55
|
+
return globalThis.crypto.randomUUID();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback UUID v4 generator
|
|
59
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
60
|
+
const r = Math.random() * 16 | 0;
|
|
61
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
62
|
+
return v.toString(16);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { YlsooCrypto };
|