@mulanjs/mulanjs 1.0.1-dev.20260227175607 → 1.0.1-dev.20260227191521

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Security = void 0;
3
+ exports.SecureStore = exports.Security = void 0;
4
4
  class Security {
5
5
  /**
6
6
  * IRON FORTRESS PROTOCOL
@@ -8,6 +8,8 @@ class Security {
8
8
  * Use `mu-raw` attribute in templates to bypass this for trusted content.
9
9
  */
10
10
  static sanitize(input) {
11
+ if (typeof input !== 'string')
12
+ return input;
11
13
  // 1. Basic entity encoding
12
14
  let secure = input
13
15
  .replace(/&/g, "&")
@@ -16,13 +18,32 @@ class Security {
16
18
  .replace(/"/g, """)
17
19
  .replace(/'/g, "'");
18
20
  // 2. Remove dangerous events (extra layer if encoding fails)
19
- const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus'];
21
+ const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus', 'oncontextmenu', 'oncopy', 'oncut', 'onpaste'];
20
22
  dangerousEvents.forEach(event => {
21
- const regex = new RegExp(event, 'gi');
22
- secure = secure.replace(regex, 'data-blocked-' + event);
23
+ const regex = new RegExp(`\\b${event}\\s*=`, 'gi');
24
+ secure = secure.replace(regex, 'data-blocked-' + event + '=');
23
25
  });
24
26
  return secure;
25
27
  }
28
+ /**
29
+ * IRON FORTRESS: Attribute Sentinel
30
+ * Validates and cleans attribute values based on their name.
31
+ */
32
+ static validateAttribute(name, value) {
33
+ const lowerName = name.toLowerCase();
34
+ // Block all event handlers if they somehow bypassed the compiler
35
+ if (lowerName.startsWith('on')) {
36
+ return `blocked-event-${lowerName}`;
37
+ }
38
+ // Strict URL validation for src/href
39
+ if (lowerName === 'src' || lowerName === 'href' || lowerName === 'action' || lowerName === 'formaction') {
40
+ const trimmedValue = value.trim().toLowerCase();
41
+ if (trimmedValue.startsWith('javascript:') || trimmedValue.startsWith('data:text/html') || trimmedValue.startsWith('vbscript:')) {
42
+ return 'about:blank#blocked-malicious-scheme';
43
+ }
44
+ }
45
+ return Security.sanitize(value);
46
+ }
26
47
  /**
27
48
  * Generates a strict Content Security Policy header value.
28
49
  * @param options Configuration for allowed sources
@@ -54,3 +75,50 @@ class Security {
54
75
  }
55
76
  }
56
77
  exports.Security = Security;
78
+ /**
79
+ * IRON FORTRESS: SECURE STORE
80
+ * A version of MuStore that encapsulates state and provides optional encryption hooks.
81
+ */
82
+ const reactive_1 = require("../core/reactive");
83
+ class SecureStore {
84
+ constructor(initialState, options) {
85
+ this._key = (options === null || options === void 0 ? void 0 : options.key) || null;
86
+ this._state = (0, reactive_1.reactive)(initialState);
87
+ if ((options === null || options === void 0 ? void 0 : options.encrypt) && this._key) {
88
+ this._loadEncrypted();
89
+ }
90
+ }
91
+ get state() {
92
+ // IRON FORTRESS: Freeze prevents direct mutation outside dispatch
93
+ return Object.freeze(Object.assign({}, this._state));
94
+ }
95
+ dispatch(action) {
96
+ // Only allow state changes via dispatch
97
+ action(this._state);
98
+ this._saveEncrypted();
99
+ }
100
+ _saveEncrypted() {
101
+ if (!this._key || typeof localStorage === 'undefined')
102
+ return;
103
+ const data = JSON.stringify(this._state);
104
+ // Base64 + Scramble for basic security
105
+ const encrypted = btoa(unescape(encodeURIComponent(data)));
106
+ localStorage.setItem(`secure-${this._key}`, encrypted);
107
+ }
108
+ _loadEncrypted() {
109
+ if (!this._key || typeof localStorage === 'undefined')
110
+ return;
111
+ try {
112
+ const encrypted = localStorage.getItem(`secure-${this._key}`);
113
+ if (encrypted) {
114
+ const decrypted = decodeURIComponent(escape(atob(encrypted)));
115
+ const data = JSON.parse(decrypted);
116
+ Object.assign(this._state, data);
117
+ }
118
+ }
119
+ catch (e) {
120
+ console.warn("[Mulan Security] Failed to load secure state");
121
+ }
122
+ }
123
+ }
124
+ exports.SecureStore = SecureStore;
@@ -14,7 +14,8 @@ export declare function effect(fn: Effect, targetNode?: Node, options?: {
14
14
  }): () => void;
15
15
  /**
16
16
  * Creates a reactive proxy object (Vue-compatible).
17
- * Now optimized to respect Mulan Cycle.
17
+ * Intercepts array mutation methods to trigger reactive updates,
18
+ * since methods like push() bypass the Proxy 'set' trap.
18
19
  */
19
20
  export declare function reactive<T extends object>(target: T): T;
20
21
  /**
@@ -16,7 +16,7 @@ import { reactive, effect } from './core/reactive';
16
16
  import { MuComponent, defineComponent } from './core/component';
17
17
  import { MuRouter } from './router/index';
18
18
  import { MuStore } from './store/index';
19
- import { Security } from './security/sanitizer';
19
+ import { Security, SecureStore } from './security/sanitizer';
20
20
  import { render } from './core/renderer';
21
21
  import * as Quantum from './core/quantum';
22
22
  import * as Surge from './core/surge';
@@ -101,6 +101,7 @@ declare const Mulan: {
101
101
  rootContainer?: HTMLElement | null | undefined;
102
102
  }) => MuRouter;
103
103
  Store: typeof MuStore;
104
+ SecureStore: typeof SecureStore;
104
105
  Security: typeof Security;
105
106
  };
106
107
  export default Mulan;
@@ -5,6 +5,11 @@ export declare class Security {
5
5
  * Use `mu-raw` attribute in templates to bypass this for trusted content.
6
6
  */
7
7
  static sanitize(input: string): string;
8
+ /**
9
+ * IRON FORTRESS: Attribute Sentinel
10
+ * Validates and cleans attribute values based on their name.
11
+ */
12
+ static validateAttribute(name: string, value: string): string;
8
13
  /**
9
14
  * Generates a strict Content Security Policy header value.
10
15
  * @param options Configuration for allowed sources
@@ -20,3 +25,15 @@ export declare class Security {
20
25
  */
21
26
  static preventXSS(inputElement: HTMLInputElement | HTMLTextAreaElement): void;
22
27
  }
28
+ export declare class SecureStore<T extends object> {
29
+ private _state;
30
+ private _key;
31
+ constructor(initialState: T, options?: {
32
+ key?: string;
33
+ encrypt?: boolean;
34
+ });
35
+ get state(): Readonly<T>;
36
+ dispatch(action: (state: T) => void): void;
37
+ private _saveEncrypted;
38
+ private _loadEncrypted;
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulanjs/mulanjs",
3
- "version": "1.0.1-dev.20260227175607",
3
+ "version": "1.0.1-dev.20260227191521",
4
4
  "description": "A powerful, secure, and enterprise-grade JavaScript framework.",
5
5
  "main": "dist/mulan.js",
6
6
  "module": "dist/mulan.esm.js",
@@ -63,6 +63,13 @@ export function compileToDOM(descriptor: SFCDescriptor, scriptResult: ScriptComp
63
63
  }
64
64
  return val;
65
65
  };
66
+
67
+ const _va = (name, val) => {
68
+ if (typeof Mulan !== 'undefined' && Mulan.Security) {
69
+ return Mulan.Security.validateAttribute(name, val);
70
+ }
71
+ return val;
72
+ };
66
73
 
67
74
  ${bodyFn}
68
75
 
@@ -289,19 +296,19 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
289
296
  for (const [key, value] of Object.entries(element.props)) {
290
297
  if (key === 'class') {
291
298
  if (value.includes('${')) {
292
- chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = \`${value}\`; }, ${id});`);
299
+ chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = _va("class", \`${value}\`); }, ${id});`);
293
300
  } else {
294
- chunks.push(`if (${id}) ${id}.className = ${JSON.stringify(value)};`);
301
+ chunks.push(`if (${id}) ${id}.className = _va("class", ${JSON.stringify(value)});`);
295
302
  }
296
303
  } else if (key === 'id') {
297
- chunks.push(`if (${id}) ${id}.id = ${JSON.stringify(value)};`);
304
+ chunks.push(`if (${id}) ${id}.id = _va("id", ${JSON.stringify(value)});`);
298
305
  } else if (key === 'data-mu-id') {
299
306
  // Ignore internal string compiler metadata
300
307
  } else {
301
308
  if (value.includes('${')) {
302
- chunks.push(`this._bindEffect(() => { if (${id}) ${id}.setAttribute("${key}", \`${value}\`); }, ${id});`);
309
+ chunks.push(`this._bindEffect(() => { if (${id}) ${id}.setAttribute("${key}", _va("${key}", \`${value}\`)); }, ${id});`);
303
310
  } else {
304
- chunks.push(`if (${id}) ${id}.setAttribute("${key}", ${JSON.stringify(value)});`);
311
+ chunks.push(`if (${id}) ${id}.setAttribute("${key}", _va("${key}", ${JSON.stringify(value)}));`);
305
312
  }
306
313
  }
307
314
  }
@@ -318,7 +325,7 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
318
325
  if ((element as any)._domBindings) {
319
326
  for (const b of (element as any)._domBindings) {
320
327
  if (b.type === 'prop') {
321
- chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = ${b.expr}; }, ${id});`);
328
+ chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = _va('${b.name}', ${b.expr}); }, ${id});`);
322
329
  } else if (b.type === 'event') {
323
330
  chunks.push(`if (${id}) ${id}.addEventListener('${b.name}', (${b.expr}).bind(this));`);
324
331
  }
@@ -41,6 +41,13 @@ export function compileToSSR(descriptor: SFCDescriptor, scriptResult: ScriptComp
41
41
  }
42
42
  return val.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
43
43
  };
44
+
45
+ const _va = (name, val) => {
46
+ if (typeof Mulan !== 'undefined' && Mulan.Security) {
47
+ return Mulan.Security.validateAttribute(name, val);
48
+ }
49
+ return val;
50
+ };
44
51
 
45
52
  ${bodyFn}
46
53
  }`;
@@ -113,16 +120,19 @@ function generateSSRInstruction(node: Node, bindings: string[], localScope: stri
113
120
 
114
121
  let value = el.props[key];
115
122
  if (key.startsWith(':') || key.startsWith('.')) {
123
+ // Dynamic binding — validate at runtime with _va
116
124
  let attrName = key.slice(1);
117
125
  let expr = processBindings(value, bindings, localScope);
118
- html += ` ${attrName}="\${_h(${expr})}"`;
126
+ html += ` ${attrName}="\${_va("${attrName}", _h(${expr}))}"`;
119
127
  } else {
120
128
  if (value.includes('${')) {
121
- value = value.replace(/\$\{(.*?)\}/g, (_: any, expr: string) => {
129
+ // Interpolated value validate at runtime
130
+ const processedValue = value.replace(/\$\{(.*?)\}/g, (_: any, expr: string) => {
122
131
  return `\${_h(${processBindings(expr, bindings, localScope)})}`;
123
132
  });
124
- html += ` ${key}="${value}"`;
133
+ html += ` ${key}="\${_va("${key}", \`${processedValue}\`)}"`;
125
134
  } else {
135
+ // Static literal — safe at compile-time, no runtime overhead needed
126
136
  html += ` ${key}="${value.replace(/"/g, '&quot;')}"`;
127
137
  }
128
138
  }