@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.
@@ -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}\`; }, ${id});`);
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
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
- value = value.replace(/\$\{(.*?)\}/g, (_, expr) => {
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}="${value}"`;
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, '&quot;')}"`;
116
126
  }
117
127
  }
@@ -146,12 +146,23 @@ function trigger(target, key) {
146
146
  });
147
147
  }
148
148
  }
149
+ // Array mutating methods that must trigger reactive updates
150
+ const ARRAY_MUTATION_METHODS = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill'];
151
+ // Proxy cache: ensures the same object always returns the same Proxy
152
+ // This is CRITICAL for array reactivity — mu-for and array mutations must
153
+ // share the same Proxy instance, otherwise triggers reach different subscribers.
154
+ const proxyCache = new WeakMap();
149
155
  /**
150
156
  * Creates a reactive proxy object (Vue-compatible).
151
- * Now optimized to respect Mulan Cycle.
157
+ * Intercepts array mutation methods to trigger reactive updates,
158
+ * since methods like push() bypass the Proxy 'set' trap.
152
159
  */
153
160
  function reactive(target) {
154
- return new Proxy(target, {
161
+ // Return cached proxy if it already exists for this target
162
+ if (proxyCache.has(target)) {
163
+ return proxyCache.get(target);
164
+ }
165
+ const proxy = new Proxy(target, {
155
166
  get(obj, prop, receiver) {
156
167
  // IRON FORTRESS: Prototype Pollution Protection (Read)
157
168
  if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') {
@@ -159,6 +170,17 @@ function reactive(target) {
159
170
  }
160
171
  track(obj, prop);
161
172
  const val = Reflect.get(obj, prop, receiver);
173
+ // Intercept array mutation methods to trigger length + index updates
174
+ if (Array.isArray(obj) && typeof prop === 'string' && ARRAY_MUTATION_METHODS.includes(prop)) {
175
+ return function (...args) {
176
+ const result = val.apply(obj, args);
177
+ // Trigger on 'length' — this is what mu-for subscribes to
178
+ trigger(obj, 'length');
179
+ // Also trigger on the array itself for any parent effects
180
+ trigger(obj, prop);
181
+ return result;
182
+ };
183
+ }
162
184
  if (val !== null && typeof val === 'object') {
163
185
  return reactive(val);
164
186
  }
@@ -172,9 +194,15 @@ function reactive(target) {
172
194
  }
173
195
  const result = Reflect.set(obj, prop, value, receiver);
174
196
  trigger(obj, prop);
197
+ // If array length changed (e.g. index assignment), also trigger length
198
+ if (Array.isArray(obj) && prop !== 'length') {
199
+ trigger(obj, 'length');
200
+ }
175
201
  return result;
176
202
  },
177
203
  });
204
+ proxyCache.set(target, proxy);
205
+ return proxy;
178
206
  }
179
207
  exports.reactive = reactive;
180
208
  /**
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
@@ -242,12 +242,23 @@ function trigger(target, key) {
242
242
  });
243
243
  }
244
244
  }
245
+ // Array mutating methods that must trigger reactive updates
246
+ const ARRAY_MUTATION_METHODS = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill'];
247
+ // Proxy cache: ensures the same object always returns the same Proxy
248
+ // This is CRITICAL for array reactivity — mu-for and array mutations must
249
+ // share the same Proxy instance, otherwise triggers reach different subscribers.
250
+ const proxyCache = new WeakMap();
245
251
  /**
246
252
  * Creates a reactive proxy object (Vue-compatible).
247
- * Now optimized to respect Mulan Cycle.
253
+ * Intercepts array mutation methods to trigger reactive updates,
254
+ * since methods like push() bypass the Proxy 'set' trap.
248
255
  */
249
256
  function reactive(target) {
250
- return new Proxy(target, {
257
+ // Return cached proxy if it already exists for this target
258
+ if (proxyCache.has(target)) {
259
+ return proxyCache.get(target);
260
+ }
261
+ const proxy = new Proxy(target, {
251
262
  get(obj, prop, receiver) {
252
263
  // IRON FORTRESS: Prototype Pollution Protection (Read)
253
264
  if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') {
@@ -255,6 +266,17 @@ function reactive(target) {
255
266
  }
256
267
  track(obj, prop);
257
268
  const val = Reflect.get(obj, prop, receiver);
269
+ // Intercept array mutation methods to trigger length + index updates
270
+ if (Array.isArray(obj) && typeof prop === 'string' && ARRAY_MUTATION_METHODS.includes(prop)) {
271
+ return function (...args) {
272
+ const result = val.apply(obj, args);
273
+ // Trigger on 'length' — this is what mu-for subscribes to
274
+ trigger(obj, 'length');
275
+ // Also trigger on the array itself for any parent effects
276
+ trigger(obj, prop);
277
+ return result;
278
+ };
279
+ }
258
280
  if (val !== null && typeof val === 'object') {
259
281
  return reactive(val);
260
282
  }
@@ -268,9 +290,15 @@ function reactive(target) {
268
290
  }
269
291
  const result = Reflect.set(obj, prop, value, receiver);
270
292
  trigger(obj, prop);
293
+ // If array length changed (e.g. index assignment), also trigger length
294
+ if (Array.isArray(obj) && prop !== 'length') {
295
+ trigger(obj, 'length');
296
+ }
271
297
  return result;
272
298
  },
273
299
  });
300
+ proxyCache.set(target, proxy);
301
+ return proxy;
274
302
  }
275
303
  /**
276
304
  * Creates a standalone reactive reference.
@@ -1195,6 +1223,8 @@ class Security {
1195
1223
  * Use `mu-raw` attribute in templates to bypass this for trusted content.
1196
1224
  */
1197
1225
  static sanitize(input) {
1226
+ if (typeof input !== 'string')
1227
+ return input;
1198
1228
  // 1. Basic entity encoding
1199
1229
  let secure = input
1200
1230
  .replace(/&/g, "&amp;")
@@ -1203,13 +1233,32 @@ class Security {
1203
1233
  .replace(/"/g, "&quot;")
1204
1234
  .replace(/'/g, "&#039;");
1205
1235
  // 2. Remove dangerous events (extra layer if encoding fails)
1206
- const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus'];
1236
+ const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus', 'oncontextmenu', 'oncopy', 'oncut', 'onpaste'];
1207
1237
  dangerousEvents.forEach(event => {
1208
- const regex = new RegExp(event, 'gi');
1209
- secure = secure.replace(regex, 'data-blocked-' + event);
1238
+ const regex = new RegExp(`\\b${event}\\s*=`, 'gi');
1239
+ secure = secure.replace(regex, 'data-blocked-' + event + '=');
1210
1240
  });
1211
1241
  return secure;
1212
1242
  }
1243
+ /**
1244
+ * IRON FORTRESS: Attribute Sentinel
1245
+ * Validates and cleans attribute values based on their name.
1246
+ */
1247
+ static validateAttribute(name, value) {
1248
+ const lowerName = name.toLowerCase();
1249
+ // Block all event handlers if they somehow bypassed the compiler
1250
+ if (lowerName.startsWith('on')) {
1251
+ return `blocked-event-${lowerName}`;
1252
+ }
1253
+ // Strict URL validation for src/href
1254
+ if (lowerName === 'src' || lowerName === 'href' || lowerName === 'action' || lowerName === 'formaction') {
1255
+ const trimmedValue = value.trim().toLowerCase();
1256
+ if (trimmedValue.startsWith('javascript:') || trimmedValue.startsWith('data:text/html') || trimmedValue.startsWith('vbscript:')) {
1257
+ return 'about:blank#blocked-malicious-scheme';
1258
+ }
1259
+ }
1260
+ return Security.sanitize(value);
1261
+ }
1213
1262
  /**
1214
1263
  * Generates a strict Content Security Policy header value.
1215
1264
  * @param options Configuration for allowed sources
@@ -1240,6 +1289,52 @@ class Security {
1240
1289
  });
1241
1290
  }
1242
1291
  }
1292
+ /**
1293
+ * IRON FORTRESS: SECURE STORE
1294
+ * A version of MuStore that encapsulates state and provides optional encryption hooks.
1295
+ */
1296
+
1297
+ class SecureStore {
1298
+ constructor(initialState, options) {
1299
+ this._key = (options === null || options === void 0 ? void 0 : options.key) || null;
1300
+ this._state = reactive(initialState);
1301
+ if ((options === null || options === void 0 ? void 0 : options.encrypt) && this._key) {
1302
+ this._loadEncrypted();
1303
+ }
1304
+ }
1305
+ get state() {
1306
+ // IRON FORTRESS: Freeze prevents direct mutation outside dispatch
1307
+ return Object.freeze(Object.assign({}, this._state));
1308
+ }
1309
+ dispatch(action) {
1310
+ // Only allow state changes via dispatch
1311
+ action(this._state);
1312
+ this._saveEncrypted();
1313
+ }
1314
+ _saveEncrypted() {
1315
+ if (!this._key || typeof localStorage === 'undefined')
1316
+ return;
1317
+ const data = JSON.stringify(this._state);
1318
+ // Base64 + Scramble for basic security
1319
+ const encrypted = btoa(unescape(encodeURIComponent(data)));
1320
+ localStorage.setItem(`secure-${this._key}`, encrypted);
1321
+ }
1322
+ _loadEncrypted() {
1323
+ if (!this._key || typeof localStorage === 'undefined')
1324
+ return;
1325
+ try {
1326
+ const encrypted = localStorage.getItem(`secure-${this._key}`);
1327
+ if (encrypted) {
1328
+ const decrypted = decodeURIComponent(escape(atob(encrypted)));
1329
+ const data = JSON.parse(decrypted);
1330
+ Object.assign(this._state, data);
1331
+ }
1332
+ }
1333
+ catch (e) {
1334
+ console.warn("[Mulan Security] Failed to load secure state");
1335
+ }
1336
+ }
1337
+ }
1243
1338
 
1244
1339
  ;// ./src/router/index.ts
1245
1340
  var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
@@ -2512,7 +2607,7 @@ function renderToString(ComponentClass, props = {}) {
2512
2607
 
2513
2608
 
2514
2609
  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,
2610
+ 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
2611
  // MULAN INSIGHT: Branded Logging
2517
2612
  log: (msg, ...args) => {
2518
2613
  console.log(`%c[MulanJS]%c ${msg}`, "color: #ff3e00; font-weight: bold; background: #222; padding: 2px 4px; border-radius: 3px;", "", ...args);
@@ -2546,6 +2641,6 @@ if (typeof window !== 'undefined') {
2546
2641
  }
2547
2642
  /* harmony default export */ const src = (Mulan);
2548
2643
 
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 };
2644
+ 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
2645
 
2551
2646
  //# sourceMappingURL=mulan.esm.js.map