@getforma/core 0.2.0 → 0.3.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.
Files changed (38) hide show
  1. package/README.md +265 -13
  2. package/dist/{chunk-VKKPX4YU.js → chunk-5H52PKGF.js} +62 -12
  3. package/dist/chunk-5H52PKGF.js.map +1 -0
  4. package/dist/{chunk-VP5EOGM5.cjs → chunk-TSQ7AKFT.cjs} +62 -11
  5. package/dist/chunk-TSQ7AKFT.cjs.map +1 -0
  6. package/dist/forma-runtime-csp.js +2 -0
  7. package/dist/forma-runtime.js +2 -0
  8. package/dist/formajs-runtime-hardened.global.js +1 -1
  9. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  10. package/dist/formajs-runtime.global.js +1 -1
  11. package/dist/formajs-runtime.global.js.map +1 -1
  12. package/dist/formajs.global.js +1 -1
  13. package/dist/formajs.global.js.map +1 -1
  14. package/dist/index.cjs +100 -69
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +43 -27
  17. package/dist/index.d.ts +43 -27
  18. package/dist/index.js +57 -27
  19. package/dist/index.js.map +1 -1
  20. package/dist/runtime-hardened.cjs +166 -50
  21. package/dist/runtime-hardened.cjs.map +1 -1
  22. package/dist/runtime-hardened.d.cts +86 -0
  23. package/dist/runtime-hardened.d.ts +86 -0
  24. package/dist/runtime-hardened.js +166 -50
  25. package/dist/runtime-hardened.js.map +1 -1
  26. package/dist/runtime.cjs +188 -72
  27. package/dist/runtime.cjs.map +1 -1
  28. package/dist/runtime.js +167 -51
  29. package/dist/runtime.js.map +1 -1
  30. package/dist/ssr/index.cjs +59 -71
  31. package/dist/ssr/index.cjs.map +1 -1
  32. package/dist/ssr/index.d.cts +4 -0
  33. package/dist/ssr/index.d.ts +4 -0
  34. package/dist/ssr/index.js +59 -71
  35. package/dist/ssr/index.js.map +1 -1
  36. package/package.json +26 -3
  37. package/dist/chunk-VKKPX4YU.js.map +0 -1
  38. package/dist/chunk-VP5EOGM5.cjs.map +0 -1
package/dist/runtime.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkVP5EOGM5_cjs = require('./chunk-VP5EOGM5.cjs');
3
+ var chunkTSQ7AKFT_cjs = require('./chunk-TSQ7AKFT.cjs');
4
4
  var chunkFPSLC62A_cjs = require('./chunk-FPSLC62A.cjs');
5
5
 
6
6
  // src/dom/reconcile.ts
@@ -481,6 +481,7 @@ if (buildUnsafeEvalMode) {
481
481
  _unsafeEvalMode = buildUnsafeEvalMode;
482
482
  if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
483
483
  if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
484
+ if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
484
485
  }
485
486
  var runtimeConfig = readRuntimeConfig();
486
487
  var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
@@ -558,7 +559,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
558
559
  var RE_NUMBER = /^-?\d+(\.\d+)?$/;
559
560
  var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
560
561
  var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
561
- var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
562
562
  var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
563
563
  var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
564
564
  var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
@@ -569,7 +569,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
569
569
  var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
570
570
  var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
571
571
  var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
572
- var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
573
572
  var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
574
573
  var RE_STRIP_BRACES = /^\{|\}$/g;
575
574
  var RE_DIGIT_ONLY = /^\d+$/;
@@ -584,6 +583,7 @@ var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
584
583
  var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
585
584
  var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
586
585
  var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
586
+ var RE_EVENT_REF = /\bevent\b|\$event\b/;
587
587
  var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
588
588
  var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
589
589
  var EXPRESSION_CACHE_MAX = 2048;
@@ -598,7 +598,40 @@ function cacheExpression(key, factory) {
598
598
  var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
599
599
  var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
600
600
  var compiledTemplateCache = /* @__PURE__ */ new Map();
601
- var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "__proto__", "prototype"]);
601
+ var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
602
+ "constructor",
603
+ "__proto__",
604
+ "prototype",
605
+ "__defineGetter__",
606
+ "__defineSetter__",
607
+ "__lookupGetter__",
608
+ "__lookupSetter__",
609
+ "eval"
610
+ ]);
611
+ var BLOCKED_METHOD_REGEXES = (() => {
612
+ const result = [];
613
+ for (const name of UNSAFE_METHOD_NAMES) {
614
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
615
+ result.push({
616
+ name,
617
+ // Match as property access (.name) or bare identifier at start
618
+ dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
619
+ // Match bracket access with single quotes, double quotes, or backticks
620
+ bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
621
+ });
622
+ }
623
+ return result;
624
+ })();
625
+ function findBlockedMethod(expr) {
626
+ let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
627
+ cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
628
+ cleaned = cleaned.replace(/\s*\.\s*/g, ".");
629
+ for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
630
+ if (dotRe.test(cleaned)) return name;
631
+ if (bracketRe.test(cleaned)) return name;
632
+ }
633
+ return null;
634
+ }
602
635
  var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
603
636
  function toTextValue(value) {
604
637
  if (value == null) return "";
@@ -873,6 +906,7 @@ function consumeStatement(raw) {
873
906
  function parseIfHandler(expr, scope) {
874
907
  const input = expr.trim();
875
908
  if (!RE_IF_PREFIX.test(input)) return null;
909
+ if (RE_EVENT_REF.test(input)) return null;
876
910
  let idx = 2;
877
911
  while (idx < input.length && /\s/.test(input[idx])) idx++;
878
912
  if (input[idx] !== "(") return null;
@@ -897,7 +931,7 @@ function parseIfHandler(expr, scope) {
897
931
  }
898
932
  if (rest.length > 0) return null;
899
933
  return (e) => {
900
- chunkVP5EOGM5_cjs.batch(() => {
934
+ chunkTSQ7AKFT_cjs.batch(() => {
901
935
  if (condExpr()) thenHandler(e);
902
936
  else elseHandler?.(e);
903
937
  });
@@ -1274,6 +1308,68 @@ function cloneAttributeTemplates(el, item) {
1274
1308
  }
1275
1309
  }
1276
1310
  }
1311
+ function parseChainedAccess(expr, scope) {
1312
+ let pos = 0;
1313
+ const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
1314
+ if (!identMatch) return null;
1315
+ const rootName = identMatch[0];
1316
+ pos = rootName.length;
1317
+ if (pos >= expr.length) return null;
1318
+ if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
1319
+ const steps = [];
1320
+ while (pos < expr.length) {
1321
+ let optional = false;
1322
+ if (expr[pos] === "?" && expr[pos + 1] === ".") {
1323
+ optional = true;
1324
+ pos += 2;
1325
+ } else if (expr[pos] === ".") {
1326
+ pos += 1;
1327
+ } else {
1328
+ return null;
1329
+ }
1330
+ const nameMatch = expr.slice(pos).match(/^\w+/);
1331
+ if (!nameMatch) return null;
1332
+ const name = nameMatch[0];
1333
+ pos += name.length;
1334
+ if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
1335
+ if (pos < expr.length && expr[pos] === "(") {
1336
+ const balanced = readBalancedSegment(expr, pos, "(", ")");
1337
+ if (!balanced) return null;
1338
+ const argsRaw = balanced.inner.trim();
1339
+ const argFns = [];
1340
+ for (const arg of splitCallArgs(argsRaw)) {
1341
+ const parsed = parseExpression(arg, scope);
1342
+ if (!parsed) return null;
1343
+ argFns.push(parsed);
1344
+ }
1345
+ steps.push({ type: "call", name, optional, argFns });
1346
+ pos = balanced.end + 1;
1347
+ } else {
1348
+ steps.push({ type: "prop", name, optional });
1349
+ }
1350
+ }
1351
+ if (pos !== expr.length) return null;
1352
+ if (steps.length === 0) return null;
1353
+ const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
1354
+ return () => {
1355
+ let val = rootExpr();
1356
+ for (const step of steps) {
1357
+ if (val == null) {
1358
+ if (step.optional) return void 0;
1359
+ return void 0;
1360
+ }
1361
+ if (step.type === "prop") {
1362
+ val = val[step.name];
1363
+ } else {
1364
+ const method = val[step.name];
1365
+ if (typeof method !== "function") return void 0;
1366
+ const args = step.argFns.map((fn) => fn());
1367
+ val = method.apply(val, args);
1368
+ }
1369
+ }
1370
+ return val;
1371
+ };
1372
+ }
1277
1373
  function parseExpression(expr, scope) {
1278
1374
  const cachedFactory = expressionCache.get(expr);
1279
1375
  if (cachedFactory) return cachedFactory(scope);
@@ -1324,24 +1420,9 @@ function parseExpressionUncached(expr, scope) {
1324
1420
  if (RE_IDENTIFIER.test(expr)) {
1325
1421
  return () => scope.getters[expr]?.();
1326
1422
  }
1327
- const dotMatch = expr.match(RE_DOT_ACCESS);
1328
- if (dotMatch) {
1329
- const p1 = dotMatch[1], p2 = dotMatch[2];
1330
- return () => {
1331
- const obj = scope.getters[p1]?.();
1332
- return obj?.[p2];
1333
- };
1334
- }
1335
- const deepDotMatch = expr.match(RE_DEEP_DOT);
1336
- if (deepDotMatch) {
1337
- const p1 = deepDotMatch[1], p2 = deepDotMatch[2], p3 = deepDotMatch[3], p4 = deepDotMatch[4];
1338
- return () => {
1339
- let val = scope.getters[p1]?.();
1340
- val = val?.[p2];
1341
- val = val?.[p3];
1342
- if (p4) val = val?.[p4];
1343
- return val;
1344
- };
1423
+ {
1424
+ const chainResult = parseChainedAccess(expr, scope);
1425
+ if (chainResult) return chainResult;
1345
1426
  }
1346
1427
  const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
1347
1428
  if (groupedCallMatch) {
@@ -1365,28 +1446,6 @@ function parseExpressionUncached(expr, scope) {
1365
1446
  return method.apply(base, args);
1366
1447
  };
1367
1448
  }
1368
- const callMatch = expr.match(RE_METHOD_CALL);
1369
- if (callMatch) {
1370
- const baseName = callMatch[1];
1371
- const methodName = callMatch[2];
1372
- const argsRaw = callMatch[3].trim();
1373
- if (UNSAFE_METHOD_NAMES.has(methodName)) return () => void 0;
1374
- const baseExpr = baseName === "Math" ? (() => Math) : parseExpression(baseName, scope);
1375
- if (!baseExpr) return null;
1376
- const argFns = [];
1377
- for (const arg of splitCallArgs(argsRaw)) {
1378
- const parsed = parseExpression(arg, scope);
1379
- if (!parsed) return null;
1380
- argFns.push(parsed);
1381
- }
1382
- return () => {
1383
- const base = baseExpr();
1384
- const method = base?.[methodName];
1385
- if (typeof method !== "function") return void 0;
1386
- const args = argFns.map((fn) => fn());
1387
- return method.apply(base, args);
1388
- };
1389
- }
1390
1449
  if (expr.startsWith("!")) {
1391
1450
  const inner = parseExpression(expr.slice(1).trim(), scope);
1392
1451
  if (inner) return () => !inner();
@@ -1405,6 +1464,29 @@ function parseExpressionUncached(expr, scope) {
1405
1464
  return () => objExpr()?.[key];
1406
1465
  }
1407
1466
  }
1467
+ if (expr.startsWith("[")) {
1468
+ const balanced = readBalancedSegment(expr, 0, "[", "]");
1469
+ if (balanced && balanced.end === expr.length - 1) {
1470
+ const inner = balanced.inner.trim();
1471
+ if (inner === "") {
1472
+ return () => [];
1473
+ }
1474
+ const elements = splitCallArgs(inner);
1475
+ const elementFns = [];
1476
+ let allParsed = true;
1477
+ for (const el of elements) {
1478
+ const parsed = parseExpression(el.trim(), scope);
1479
+ if (!parsed) {
1480
+ allParsed = false;
1481
+ break;
1482
+ }
1483
+ elementFns.push(parsed);
1484
+ }
1485
+ if (allParsed) {
1486
+ return () => elementFns.map((fn) => fn());
1487
+ }
1488
+ }
1489
+ }
1408
1490
  const ternaryMatch = expr.match(RE_TERNARY);
1409
1491
  if (ternaryMatch) {
1410
1492
  const cond = parseExpression(ternaryMatch[1].trim(), scope);
@@ -1534,6 +1616,15 @@ function getScopeCache(cache, scope) {
1534
1616
  }
1535
1617
  return scoped;
1536
1618
  }
1619
+ function cspExpressionHint(expr) {
1620
+ if (expr.includes("...")) {
1621
+ return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
1622
+ }
1623
+ if (expr.includes("=>")) {
1624
+ return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
1625
+ }
1626
+ return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
1627
+ }
1537
1628
  function buildEvaluator(expr, scope) {
1538
1629
  const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1539
1630
  const cache = getScopeCache(scopeExpressionCache, scope);
@@ -1546,11 +1637,17 @@ function buildEvaluator(expr, scope) {
1546
1637
  }
1547
1638
  if (!_allowUnsafeEval) {
1548
1639
  dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
1549
- reportDiagnostic("expression-unsupported", cleaned, "Unsupported expression in CSP-safe mode");
1640
+ reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
1550
1641
  const blocked = () => void 0;
1551
1642
  cache.set(cleaned, blocked);
1552
1643
  return blocked;
1553
1644
  }
1645
+ const blockedMethod = findBlockedMethod(cleaned);
1646
+ if (blockedMethod) {
1647
+ const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
1648
+ reportDiagnostic("expression-unsupported", cleaned, msg);
1649
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1650
+ }
1554
1651
  try {
1555
1652
  const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
1556
1653
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1566,7 +1663,7 @@ function buildEvaluator(expr, scope) {
1566
1663
  cache.set(cleaned, unsafe);
1567
1664
  return unsafe;
1568
1665
  } catch {
1569
- reportDiagnostic("expression-unsupported", cleaned, "Expression failed to compile");
1666
+ reportDiagnostic("expression-unsupported", cleaned, "Expression too complex for CSP-safe mode. Enable unsafe-eval via FormaRuntime.unsafeEval = true, or use the standard (non-hardened) build.");
1570
1667
  const failed = () => void 0;
1571
1668
  cache.set(cleaned, failed);
1572
1669
  return failed;
@@ -1582,7 +1679,7 @@ function parseHandler(expr, scope) {
1582
1679
  const handlers = stmts.map((s) => parseHandler(s, scope));
1583
1680
  if (handlers.every((h) => h !== null)) {
1584
1681
  return (e) => {
1585
- chunkVP5EOGM5_cjs.batch(() => {
1682
+ chunkTSQ7AKFT_cjs.batch(() => {
1586
1683
  for (const h of handlers) h(e);
1587
1684
  });
1588
1685
  };
@@ -1595,7 +1692,7 @@ function parseHandler(expr, scope) {
1595
1692
  const name = incrMatch[1];
1596
1693
  const op = incrMatch[2];
1597
1694
  return () => {
1598
- chunkVP5EOGM5_cjs.batch(() => {
1695
+ chunkTSQ7AKFT_cjs.batch(() => {
1599
1696
  const val = scope.getters[name]?.() ?? 0;
1600
1697
  scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
1601
1698
  });
@@ -1606,7 +1703,7 @@ function parseHandler(expr, scope) {
1606
1703
  const op = preIncrMatch[1];
1607
1704
  const name = preIncrMatch[2];
1608
1705
  return () => {
1609
- chunkVP5EOGM5_cjs.batch(() => {
1706
+ chunkTSQ7AKFT_cjs.batch(() => {
1610
1707
  const val = scope.getters[name]?.() ?? 0;
1611
1708
  scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
1612
1709
  });
@@ -1616,7 +1713,7 @@ function parseHandler(expr, scope) {
1616
1713
  if (toggleMatch && toggleMatch[1] === toggleMatch[2]) {
1617
1714
  const name = toggleMatch[1];
1618
1715
  return () => {
1619
- chunkVP5EOGM5_cjs.batch(() => {
1716
+ chunkTSQ7AKFT_cjs.batch(() => {
1620
1717
  scope.setters[name]?.(!scope.getters[name]?.());
1621
1718
  });
1622
1719
  };
@@ -1628,7 +1725,7 @@ function parseHandler(expr, scope) {
1628
1725
  if (valExpr) {
1629
1726
  if (_debug) dbg(`parseHandler: assignment "${name} = ..." \u2014 setter exists:`, !!scope.setters[name], ", getter exists:", !!scope.getters[name]);
1630
1727
  return () => {
1631
- chunkVP5EOGM5_cjs.batch(() => {
1728
+ chunkTSQ7AKFT_cjs.batch(() => {
1632
1729
  const val = valExpr();
1633
1730
  if (_debug) dbg(`SETTER: ${name} = ${val} (was: ${scope.getters[name]?.()})`);
1634
1731
  scope.setters[name]?.(val);
@@ -1643,7 +1740,7 @@ function parseHandler(expr, scope) {
1643
1740
  const valExpr = parseExpression(compoundMatch[3].trim(), scope);
1644
1741
  if (valExpr) {
1645
1742
  return () => {
1646
- chunkVP5EOGM5_cjs.batch(() => {
1743
+ chunkTSQ7AKFT_cjs.batch(() => {
1647
1744
  const current = scope.getters[name]?.() ?? 0;
1648
1745
  const val = valExpr();
1649
1746
  switch (op) {
@@ -1672,7 +1769,13 @@ function parseHandler(expr, scope) {
1672
1769
  return null;
1673
1770
  }
1674
1771
  function buildHandler(expr, scope) {
1675
- const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1772
+ let cleaned = expr.trim();
1773
+ if (cleaned.startsWith("{")) {
1774
+ const seg = readBalancedSegment(cleaned, 0, "{", "}");
1775
+ if (seg && seg.end === cleaned.length - 1) {
1776
+ cleaned = seg.inner.trim();
1777
+ }
1778
+ }
1676
1779
  const cache = getScopeCache(scopeHandlerCache, scope);
1677
1780
  const cached = cache.get(cleaned);
1678
1781
  if (cached) return cached;
@@ -1684,7 +1787,7 @@ function buildHandler(expr, scope) {
1684
1787
  }
1685
1788
  if (!_allowUnsafeEval) {
1686
1789
  dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
1687
- reportDiagnostic("handler-unsupported", cleaned, "Unsupported handler in CSP-safe mode");
1790
+ reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
1688
1791
  const result = {
1689
1792
  handler: () => {
1690
1793
  },
@@ -1693,6 +1796,12 @@ function buildHandler(expr, scope) {
1693
1796
  cache.set(cleaned, result);
1694
1797
  return result;
1695
1798
  }
1799
+ const blockedMethod = findBlockedMethod(cleaned);
1800
+ if (blockedMethod) {
1801
+ const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
1802
+ reportDiagnostic("handler-unsupported", cleaned, msg);
1803
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1804
+ }
1696
1805
  try {
1697
1806
  const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
1698
1807
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1711,7 +1820,7 @@ function buildHandler(expr, scope) {
1711
1820
  }
1712
1821
  });
1713
1822
  const unsafeHandler = (e) => {
1714
- chunkVP5EOGM5_cjs.batch(() => fn(proxy, e, e));
1823
+ chunkTSQ7AKFT_cjs.batch(() => fn(proxy, e, e));
1715
1824
  };
1716
1825
  const result = {
1717
1826
  handler: unsafeHandler,
@@ -1720,7 +1829,7 @@ function buildHandler(expr, scope) {
1720
1829
  cache.set(cleaned, result);
1721
1830
  return result;
1722
1831
  } catch {
1723
- reportDiagnostic("handler-unsupported", cleaned, "Handler failed to compile");
1832
+ reportDiagnostic("handler-unsupported", cleaned, "Expression too complex for CSP-safe mode. Enable unsafe-eval via FormaRuntime.unsafeEval = true, or use the standard (non-hardened) build.");
1724
1833
  const result = {
1725
1834
  handler: () => {
1726
1835
  },
@@ -1730,16 +1839,22 @@ function buildHandler(expr, scope) {
1730
1839
  return result;
1731
1840
  }
1732
1841
  }
1842
+ var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1733
1843
  function parseState(raw) {
1844
+ let parsed;
1734
1845
  try {
1735
- return JSON.parse(raw);
1846
+ parsed = JSON.parse(raw);
1736
1847
  } catch {
1737
1848
  try {
1738
- return JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
1849
+ parsed = JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
1739
1850
  } catch {
1740
1851
  return {};
1741
1852
  }
1742
1853
  }
1854
+ for (const key of FORBIDDEN_STATE_KEYS) {
1855
+ if (key in parsed) delete parsed[key];
1856
+ }
1857
+ return parsed;
1743
1858
  }
1744
1859
  function initScope(stateEl) {
1745
1860
  const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
@@ -1787,7 +1902,7 @@ function bindElement(el, scope, disposers) {
1787
1902
  const textExpr = !known || known.has("data-text") ? el.getAttribute("data-text") : null;
1788
1903
  if (textExpr) {
1789
1904
  const evaluate = buildEvaluator(textExpr, scope);
1790
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
1905
+ const dispose = chunkTSQ7AKFT_cjs.internalEffect(() => {
1791
1906
  setElementTextFast(el, toTextValue(evaluate()));
1792
1907
  });
1793
1908
  disposers.push(dispose);
@@ -1802,7 +1917,7 @@ function bindElement(el, scope, disposers) {
1802
1917
  dbg(`bindElement: data-show="${showExpr}" on <${tag}${cls}>`);
1803
1918
  }
1804
1919
  let initialized = false;
1805
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
1920
+ const dispose = chunkTSQ7AKFT_cjs.internalEffect(() => {
1806
1921
  const visible = !!evaluate();
1807
1922
  if (_debug) dbg(`data-show effect: "${showExpr}" \u2192 ${visible}`);
1808
1923
  applyShowVisibility(el, visible, transition, !initialized);
@@ -1821,7 +1936,7 @@ function bindElement(el, scope, disposers) {
1821
1936
  const parent = el.parentNode;
1822
1937
  let inserted = true;
1823
1938
  let initialized = false;
1824
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
1939
+ const dispose = chunkTSQ7AKFT_cjs.internalEffect(() => {
1825
1940
  const show = !!evaluate();
1826
1941
  if (show && !inserted) {
1827
1942
  clearTransitionState(el);
@@ -1860,7 +1975,7 @@ function bindElement(el, scope, disposers) {
1860
1975
  const setter = scope.setters[prop];
1861
1976
  if (getter && setter) {
1862
1977
  const input = el;
1863
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
1978
+ const dispose = chunkTSQ7AKFT_cjs.internalEffect(() => {
1864
1979
  const val = getter();
1865
1980
  if (input.type === "checkbox") {
1866
1981
  input.checked = !!val;
@@ -1924,14 +2039,14 @@ function bindElement(el, scope, disposers) {
1924
2039
  } else if (name.startsWith("data-class:")) {
1925
2040
  const cls = name.slice(11);
1926
2041
  const evaluate = buildEvaluator(attr.value, scope);
1927
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2042
+ const dispose = chunkTSQ7AKFT_cjs.internalEffect(() => {
1928
2043
  el.classList.toggle(cls, !!evaluate());
1929
2044
  });
1930
2045
  disposers.push(dispose);
1931
2046
  } else if (name.startsWith("data-bind:")) {
1932
2047
  const attrName = name.slice(10);
1933
2048
  const evaluate = buildEvaluator(attr.value, scope);
1934
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2049
+ const dispose = chunkTSQ7AKFT_cjs.internalEffect(() => {
1935
2050
  const val = evaluate();
1936
2051
  if (val == null || val === false) {
1937
2052
  el.removeAttribute(attrName);
@@ -1954,7 +2069,7 @@ function bindElement(el, scope, disposers) {
1954
2069
  if (saved !== null) setter(JSON.parse(saved));
1955
2070
  } catch {
1956
2071
  }
1957
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2072
+ const dispose = chunkTSQ7AKFT_cjs.internalEffect(() => {
1958
2073
  try {
1959
2074
  localStorage.setItem(key, JSON.stringify(getter()));
1960
2075
  } catch {
@@ -2041,7 +2156,7 @@ function bindElement(el, scope, disposers) {
2041
2156
  });
2042
2157
  }
2043
2158
  } : void 0;
2044
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2159
+ const dispose = chunkTSQ7AKFT_cjs.internalEffect(() => {
2045
2160
  const rawItems = evaluate();
2046
2161
  if (!Array.isArray(rawItems)) {
2047
2162
  for (const n of oldNodes) {
@@ -2062,7 +2177,7 @@ function bindElement(el, scope, disposers) {
2062
2177
  }
2063
2178
  const prevNodes = new Set(oldNodes);
2064
2179
  if (keyProp) {
2065
- const result = chunkVP5EOGM5_cjs.reconcileList(
2180
+ const result = chunkTSQ7AKFT_cjs.reconcileList(
2066
2181
  el,
2067
2182
  oldItems,
2068
2183
  rawItems,
@@ -2092,7 +2207,7 @@ function bindElement(el, scope, disposers) {
2092
2207
  } else {
2093
2208
  const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
2094
2209
  const oldWrapped = oldItems;
2095
- const result = chunkVP5EOGM5_cjs.reconcileList(
2210
+ const result = chunkTSQ7AKFT_cjs.reconcileList(
2096
2211
  el,
2097
2212
  oldWrapped,
2098
2213
  wrapped,
@@ -2409,6 +2524,7 @@ function setUnsafeEvalMode(mode) {
2409
2524
  _unsafeEvalMode = mode;
2410
2525
  if (mode === "locked-off") _allowUnsafeEval = false;
2411
2526
  if (mode === "locked-on") _allowUnsafeEval = true;
2527
+ if (mode === "mutable") _allowUnsafeEval = true;
2412
2528
  scopeExpressionCache = /* @__PURE__ */ new WeakMap();
2413
2529
  scopeHandlerCache = /* @__PURE__ */ new WeakMap();
2414
2530
  }
@@ -2461,7 +2577,7 @@ function getScopes() {
2461
2577
  function setScopeValue(element, key, value) {
2462
2578
  const scope = element.__formaScope;
2463
2579
  if (!scope?.setters[key]) return;
2464
- chunkVP5EOGM5_cjs.batch(() => {
2580
+ chunkTSQ7AKFT_cjs.batch(() => {
2465
2581
  scope.setters[key](value);
2466
2582
  });
2467
2583
  }
@@ -2470,7 +2586,7 @@ function resetScope(element) {
2470
2586
  const initialJSON = element.__formaInitialState;
2471
2587
  if (!scope || !initialJSON) return;
2472
2588
  const initial = parseState(initialJSON);
2473
- chunkVP5EOGM5_cjs.batch(() => {
2589
+ chunkTSQ7AKFT_cjs.batch(() => {
2474
2590
  for (const [key, val] of Object.entries(initial)) {
2475
2591
  scope.setters[key]?.(val);
2476
2592
  }
@@ -2500,7 +2616,7 @@ function getReconciler() {
2500
2616
  }
2501
2617
  }
2502
2618
  },
2503
- batch: chunkVP5EOGM5_cjs.batch
2619
+ batch: chunkTSQ7AKFT_cjs.batch
2504
2620
  });
2505
2621
  }
2506
2622
  return _reconciler;