@mulanjs/mulanjs 1.0.1-dev.20260227175607 → 1.0.1-dev.20260227181914
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/dist/compiler/dom-compiler.js +13 -6
- package/dist/compiler/ssr-compiler.js +13 -3
- package/dist/index.js +1 -1
- package/dist/mulan.esm.js +72 -5
- package/dist/mulan.esm.js.map +1 -1
- package/dist/mulan.js +74 -6
- package/dist/mulan.js.map +1 -1
- package/dist/security/sanitizer.js +72 -4
- package/dist/types/index.d.ts +2 -1
- package/dist/types/security/sanitizer.d.ts +17 -0
- package/package.json +1 -1
- package/src/compiler/dom-compiler.ts +13 -6
- package/src/compiler/ssr-compiler.ts +13 -3
|
@@ -47,6 +47,13 @@ function compileToDOM(descriptor, scriptResult, scopedId) {
|
|
|
47
47
|
}
|
|
48
48
|
return val;
|
|
49
49
|
};
|
|
50
|
+
|
|
51
|
+
const _va = (name, val) => {
|
|
52
|
+
if (typeof Mulan !== 'undefined' && Mulan.Security) {
|
|
53
|
+
return Mulan.Security.validateAttribute(name, val);
|
|
54
|
+
}
|
|
55
|
+
return val;
|
|
56
|
+
};
|
|
50
57
|
|
|
51
58
|
${bodyFn}
|
|
52
59
|
|
|
@@ -234,24 +241,24 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
234
241
|
for (const [key, value] of Object.entries(element.props)) {
|
|
235
242
|
if (key === 'class') {
|
|
236
243
|
if (value.includes('${')) {
|
|
237
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = \`${value}
|
|
244
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = _va("class", \`${value}\`); }, ${id});`);
|
|
238
245
|
}
|
|
239
246
|
else {
|
|
240
|
-
chunks.push(`if (${id}) ${id}.className = ${JSON.stringify(value)};`);
|
|
247
|
+
chunks.push(`if (${id}) ${id}.className = _va("class", ${JSON.stringify(value)});`);
|
|
241
248
|
}
|
|
242
249
|
}
|
|
243
250
|
else if (key === 'id') {
|
|
244
|
-
chunks.push(`if (${id}) ${id}.id = ${JSON.stringify(value)};`);
|
|
251
|
+
chunks.push(`if (${id}) ${id}.id = _va("id", ${JSON.stringify(value)});`);
|
|
245
252
|
}
|
|
246
253
|
else if (key === 'data-mu-id') {
|
|
247
254
|
// Ignore internal string compiler metadata
|
|
248
255
|
}
|
|
249
256
|
else {
|
|
250
257
|
if (value.includes('${')) {
|
|
251
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.setAttribute("${key}", \`${value}\`); }, ${id});`);
|
|
258
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.setAttribute("${key}", _va("${key}", \`${value}\`)); }, ${id});`);
|
|
252
259
|
}
|
|
253
260
|
else {
|
|
254
|
-
chunks.push(`if (${id}) ${id}.setAttribute("${key}", ${JSON.stringify(value)});`);
|
|
261
|
+
chunks.push(`if (${id}) ${id}.setAttribute("${key}", _va("${key}", ${JSON.stringify(value)}));`);
|
|
255
262
|
}
|
|
256
263
|
}
|
|
257
264
|
}
|
|
@@ -266,7 +273,7 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
266
273
|
if (element._domBindings) {
|
|
267
274
|
for (const b of element._domBindings) {
|
|
268
275
|
if (b.type === 'prop') {
|
|
269
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = ${b.expr}; }, ${id});`);
|
|
276
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = _va('${b.name}', ${b.expr}); }, ${id});`);
|
|
270
277
|
}
|
|
271
278
|
else if (b.type === 'event') {
|
|
272
279
|
chunks.push(`if (${id}) ${id}.addEventListener('${b.name}', (${b.expr}).bind(this));`);
|
|
@@ -32,6 +32,13 @@ function compileToSSR(descriptor, scriptResult, scopedId) {
|
|
|
32
32
|
}
|
|
33
33
|
return val.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
34
34
|
};
|
|
35
|
+
|
|
36
|
+
const _va = (name, val) => {
|
|
37
|
+
if (typeof Mulan !== 'undefined' && Mulan.Security) {
|
|
38
|
+
return Mulan.Security.validateAttribute(name, val);
|
|
39
|
+
}
|
|
40
|
+
return val;
|
|
41
|
+
};
|
|
35
42
|
|
|
36
43
|
${bodyFn}
|
|
37
44
|
}`;
|
|
@@ -100,18 +107,21 @@ function generateSSRInstruction(node, bindings, localScope, getUid) {
|
|
|
100
107
|
}
|
|
101
108
|
let value = el.props[key];
|
|
102
109
|
if (key.startsWith(':') || key.startsWith('.')) {
|
|
110
|
+
// Dynamic binding — validate at runtime with _va
|
|
103
111
|
let attrName = key.slice(1);
|
|
104
112
|
let expr = processBindings(value, bindings, localScope);
|
|
105
|
-
html += ` ${attrName}="\${_h(${expr})}"`;
|
|
113
|
+
html += ` ${attrName}="\${_va("${attrName}", _h(${expr}))}"`;
|
|
106
114
|
}
|
|
107
115
|
else {
|
|
108
116
|
if (value.includes('${')) {
|
|
109
|
-
|
|
117
|
+
// Interpolated value — validate at runtime
|
|
118
|
+
const processedValue = value.replace(/\$\{(.*?)\}/g, (_, expr) => {
|
|
110
119
|
return `\${_h(${processBindings(expr, bindings, localScope)})}`;
|
|
111
120
|
});
|
|
112
|
-
html += ` ${key}="${
|
|
121
|
+
html += ` ${key}="\${_va("${key}", \`${processedValue}\`)}"`;
|
|
113
122
|
}
|
|
114
123
|
else {
|
|
124
|
+
// Static literal — safe at compile-time, no runtime overhead needed
|
|
115
125
|
html += ` ${key}="${value.replace(/"/g, '"')}"`;
|
|
116
126
|
}
|
|
117
127
|
}
|
package/dist/index.js
CHANGED
|
@@ -60,7 +60,7 @@ const Quantum = __importStar(require("./core/quantum"));
|
|
|
60
60
|
const Surge = __importStar(require("./core/surge"));
|
|
61
61
|
const InfinityList = __importStar(require("./components/infinity-list"));
|
|
62
62
|
const Mulan = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ reactive: reactive_1.reactive,
|
|
63
|
-
effect: reactive_1.effect, Component: component_2.MuComponent, defineComponent: component_2.defineComponent, Router: index_3.MuRouter, createRouter: index_3.createRouter, Store: index_4.MuStore, Security: sanitizer_1.Security }, Hooks), Query), Quantum), Surge), InfinityList), { render: renderer_1.render,
|
|
63
|
+
effect: reactive_1.effect, Component: component_2.MuComponent, defineComponent: component_2.defineComponent, Router: index_3.MuRouter, createRouter: index_3.createRouter, Store: index_4.MuStore, SecureStore: sanitizer_1.SecureStore, Security: sanitizer_1.Security }, Hooks), Query), Quantum), Surge), InfinityList), { render: renderer_1.render,
|
|
64
64
|
// MULAN INSIGHT: Branded Logging
|
|
65
65
|
log: (msg, ...args) => {
|
|
66
66
|
console.log(`%c[MulanJS]%c ${msg}`, "color: #ff3e00; font-weight: bold; background: #222; padding: 2px 4px; border-radius: 3px;", "", ...args);
|
package/dist/mulan.esm.js
CHANGED
|
@@ -1195,6 +1195,8 @@ class Security {
|
|
|
1195
1195
|
* Use `mu-raw` attribute in templates to bypass this for trusted content.
|
|
1196
1196
|
*/
|
|
1197
1197
|
static sanitize(input) {
|
|
1198
|
+
if (typeof input !== 'string')
|
|
1199
|
+
return input;
|
|
1198
1200
|
// 1. Basic entity encoding
|
|
1199
1201
|
let secure = input
|
|
1200
1202
|
.replace(/&/g, "&")
|
|
@@ -1203,13 +1205,32 @@ class Security {
|
|
|
1203
1205
|
.replace(/"/g, """)
|
|
1204
1206
|
.replace(/'/g, "'");
|
|
1205
1207
|
// 2. Remove dangerous events (extra layer if encoding fails)
|
|
1206
|
-
const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus'];
|
|
1208
|
+
const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus', 'oncontextmenu', 'oncopy', 'oncut', 'onpaste'];
|
|
1207
1209
|
dangerousEvents.forEach(event => {
|
|
1208
|
-
const regex = new RegExp(event
|
|
1209
|
-
secure = secure.replace(regex, 'data-blocked-' + event);
|
|
1210
|
+
const regex = new RegExp(`\\b${event}\\s*=`, 'gi');
|
|
1211
|
+
secure = secure.replace(regex, 'data-blocked-' + event + '=');
|
|
1210
1212
|
});
|
|
1211
1213
|
return secure;
|
|
1212
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* IRON FORTRESS: Attribute Sentinel
|
|
1217
|
+
* Validates and cleans attribute values based on their name.
|
|
1218
|
+
*/
|
|
1219
|
+
static validateAttribute(name, value) {
|
|
1220
|
+
const lowerName = name.toLowerCase();
|
|
1221
|
+
// Block all event handlers if they somehow bypassed the compiler
|
|
1222
|
+
if (lowerName.startsWith('on')) {
|
|
1223
|
+
return `blocked-event-${lowerName}`;
|
|
1224
|
+
}
|
|
1225
|
+
// Strict URL validation for src/href
|
|
1226
|
+
if (lowerName === 'src' || lowerName === 'href' || lowerName === 'action' || lowerName === 'formaction') {
|
|
1227
|
+
const trimmedValue = value.trim().toLowerCase();
|
|
1228
|
+
if (trimmedValue.startsWith('javascript:') || trimmedValue.startsWith('data:text/html') || trimmedValue.startsWith('vbscript:')) {
|
|
1229
|
+
return 'about:blank#blocked-malicious-scheme';
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return Security.sanitize(value);
|
|
1233
|
+
}
|
|
1213
1234
|
/**
|
|
1214
1235
|
* Generates a strict Content Security Policy header value.
|
|
1215
1236
|
* @param options Configuration for allowed sources
|
|
@@ -1240,6 +1261,52 @@ class Security {
|
|
|
1240
1261
|
});
|
|
1241
1262
|
}
|
|
1242
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* IRON FORTRESS: SECURE STORE
|
|
1266
|
+
* A version of MuStore that encapsulates state and provides optional encryption hooks.
|
|
1267
|
+
*/
|
|
1268
|
+
|
|
1269
|
+
class SecureStore {
|
|
1270
|
+
constructor(initialState, options) {
|
|
1271
|
+
this._key = (options === null || options === void 0 ? void 0 : options.key) || null;
|
|
1272
|
+
this._state = reactive(initialState);
|
|
1273
|
+
if ((options === null || options === void 0 ? void 0 : options.encrypt) && this._key) {
|
|
1274
|
+
this._loadEncrypted();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
get state() {
|
|
1278
|
+
// IRON FORTRESS: Freeze prevents direct mutation outside dispatch
|
|
1279
|
+
return Object.freeze(Object.assign({}, this._state));
|
|
1280
|
+
}
|
|
1281
|
+
dispatch(action) {
|
|
1282
|
+
// Only allow state changes via dispatch
|
|
1283
|
+
action(this._state);
|
|
1284
|
+
this._saveEncrypted();
|
|
1285
|
+
}
|
|
1286
|
+
_saveEncrypted() {
|
|
1287
|
+
if (!this._key || typeof localStorage === 'undefined')
|
|
1288
|
+
return;
|
|
1289
|
+
const data = JSON.stringify(this._state);
|
|
1290
|
+
// Base64 + Scramble for basic security
|
|
1291
|
+
const encrypted = btoa(unescape(encodeURIComponent(data)));
|
|
1292
|
+
localStorage.setItem(`secure-${this._key}`, encrypted);
|
|
1293
|
+
}
|
|
1294
|
+
_loadEncrypted() {
|
|
1295
|
+
if (!this._key || typeof localStorage === 'undefined')
|
|
1296
|
+
return;
|
|
1297
|
+
try {
|
|
1298
|
+
const encrypted = localStorage.getItem(`secure-${this._key}`);
|
|
1299
|
+
if (encrypted) {
|
|
1300
|
+
const decrypted = decodeURIComponent(escape(atob(encrypted)));
|
|
1301
|
+
const data = JSON.parse(decrypted);
|
|
1302
|
+
Object.assign(this._state, data);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
catch (e) {
|
|
1306
|
+
console.warn("[Mulan Security] Failed to load secure state");
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1243
1310
|
|
|
1244
1311
|
;// ./src/router/index.ts
|
|
1245
1312
|
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
@@ -2512,7 +2579,7 @@ function renderToString(ComponentClass, props = {}) {
|
|
|
2512
2579
|
|
|
2513
2580
|
|
|
2514
2581
|
const Mulan = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ reactive: reactive,
|
|
2515
|
-
effect: effect, Component: MuComponent, defineComponent: defineComponent, Router: MuRouter, createRouter: createRouter, Store: MuStore, Security: Security }, hooks_namespaceObject), query_namespaceObject), quantum_namespaceObject), surge_namespaceObject), infinity_list_namespaceObject), { render: render,
|
|
2582
|
+
effect: effect, Component: MuComponent, defineComponent: defineComponent, Router: MuRouter, createRouter: createRouter, Store: MuStore, SecureStore: SecureStore, Security: Security }, hooks_namespaceObject), query_namespaceObject), quantum_namespaceObject), surge_namespaceObject), infinity_list_namespaceObject), { render: render,
|
|
2516
2583
|
// MULAN INSIGHT: Branded Logging
|
|
2517
2584
|
log: (msg, ...args) => {
|
|
2518
2585
|
console.log(`%c[MulanJS]%c ${msg}`, "color: #ff3e00; font-weight: bold; background: #222; padding: 2px 4px; border-radius: 3px;", "", ...args);
|
|
@@ -2546,6 +2613,6 @@ if (typeof window !== 'undefined') {
|
|
|
2546
2613
|
}
|
|
2547
2614
|
/* harmony default export */ const src = (Mulan);
|
|
2548
2615
|
|
|
2549
|
-
export { MuComponent as Component, MuBlochSphereElement, MuComponent, MuInfinity, MuRouter, MuStore, MuRouter as Router, Security, Signal, activeControls, src as default, defineComponent, effect, getCurrentInstance, hydrate, muBurst, muControl, muDebounced, muEffect, muEntangle, muGate, muGeom, muHistory, muMeasure, muMemo, muParallel, muPulse, muQubit, muRegister, muSearch, muState, muSurge, muSuspense, muSwitch, muTeleport, muThrottled, muVault, nextTick, onMuDestroy, onMuIdle, onMuInit, onMuMount, onMuPanic, onMuResume, onMuShake, onMuVisibility, onMuVoice, persistent, queueEffect, reactive, ref, render, renderToString, sanitize, setCurrentInstance, useMutation, useQuery };
|
|
2616
|
+
export { MuComponent as Component, MuBlochSphereElement, MuComponent, MuInfinity, MuRouter, MuStore, MuRouter as Router, SecureStore, Security, Signal, activeControls, src as default, defineComponent, effect, getCurrentInstance, hydrate, muBurst, muControl, muDebounced, muEffect, muEntangle, muGate, muGeom, muHistory, muMeasure, muMemo, muParallel, muPulse, muQubit, muRegister, muSearch, muState, muSurge, muSuspense, muSwitch, muTeleport, muThrottled, muVault, nextTick, onMuDestroy, onMuIdle, onMuInit, onMuMount, onMuPanic, onMuResume, onMuShake, onMuVisibility, onMuVoice, persistent, queueEffect, reactive, ref, render, renderToString, sanitize, setCurrentInstance, useMutation, useQuery };
|
|
2550
2617
|
|
|
2551
2618
|
//# sourceMappingURL=mulan.esm.js.map
|