@getforma/core 0.3.0 → 0.6.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 (46) hide show
  1. package/README.md +201 -13
  2. package/dist/{chunk-KX5WRZH7.js → chunk-DPSAVBCP.js} +3 -4
  3. package/dist/chunk-DPSAVBCP.js.map +1 -0
  4. package/dist/{chunk-FPSLC62A.cjs → chunk-KH3F5NRU.cjs} +2 -4
  5. package/dist/chunk-KH3F5NRU.cjs.map +1 -0
  6. package/dist/{chunk-VKKPX4YU.js → chunk-TKGUHASG.js} +79 -19
  7. package/dist/chunk-TKGUHASG.js.map +1 -0
  8. package/dist/{chunk-VP5EOGM5.cjs → chunk-YSKF3VRA.cjs} +87 -26
  9. package/dist/chunk-YSKF3VRA.cjs.map +1 -0
  10. package/dist/forma-runtime-csp.js +2 -0
  11. package/dist/forma-runtime.js +2 -0
  12. package/dist/formajs-runtime-hardened.global.js +1 -1
  13. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  14. package/dist/formajs-runtime.global.js +1 -1
  15. package/dist/formajs-runtime.global.js.map +1 -1
  16. package/dist/formajs.global.js +1 -1
  17. package/dist/formajs.global.js.map +1 -1
  18. package/dist/index.cjs +154 -96
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +83 -35
  21. package/dist/index.d.ts +83 -35
  22. package/dist/index.js +99 -40
  23. package/dist/index.js.map +1 -1
  24. package/dist/runtime-hardened.cjs +224 -60
  25. package/dist/runtime-hardened.cjs.map +1 -1
  26. package/dist/runtime-hardened.d.cts +86 -0
  27. package/dist/runtime-hardened.d.ts +86 -0
  28. package/dist/runtime-hardened.js +225 -61
  29. package/dist/runtime-hardened.js.map +1 -1
  30. package/dist/runtime.cjs +248 -83
  31. package/dist/runtime.cjs.map +1 -1
  32. package/dist/runtime.js +226 -61
  33. package/dist/runtime.js.map +1 -1
  34. package/dist/ssr/index.cjs +69 -73
  35. package/dist/ssr/index.cjs.map +1 -1
  36. package/dist/ssr/index.d.cts +4 -0
  37. package/dist/ssr/index.d.ts +4 -0
  38. package/dist/ssr/index.js +69 -73
  39. package/dist/ssr/index.js.map +1 -1
  40. package/dist/tc39-compat.cjs +3 -3
  41. package/dist/tc39-compat.js +1 -1
  42. package/package.json +24 -3
  43. package/dist/chunk-FPSLC62A.cjs.map +0 -1
  44. package/dist/chunk-KX5WRZH7.js.map +0 -1
  45. package/dist/chunk-VKKPX4YU.js.map +0 -1
  46. package/dist/chunk-VP5EOGM5.cjs.map +0 -1
package/dist/runtime.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunkVP5EOGM5_cjs = require('./chunk-VP5EOGM5.cjs');
4
- var chunkFPSLC62A_cjs = require('./chunk-FPSLC62A.cjs');
3
+ var chunkYSKF3VRA_cjs = require('./chunk-YSKF3VRA.cjs');
4
+ var chunkKH3F5NRU_cjs = require('./chunk-KH3F5NRU.cjs');
5
5
 
6
6
  // src/dom/reconcile.ts
7
7
  function getBindTargets(el) {
@@ -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+$/;
@@ -579,11 +578,11 @@ var RE_TOGGLE = /^(\w+)\s*=\s*!(\w+)$/;
579
578
  var RE_ASSIGN = /^(\w+)\s*=\s*(.+)$/;
580
579
  var RE_COMPOUND = /^(\w+)\s*(\+=|-=|\*=|\/=)\s*(.+)$/;
581
580
  var RE_IF_PREFIX = /^if\b/;
582
- var RE_UNQUOTED_KEYS = /(\w+)\s*:/g;
583
581
  var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
584
582
  var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
585
583
  var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
586
584
  var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
585
+ var RE_EVENT_REF = /\bevent\s*[.([]|\$event\b/;
587
586
  var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
588
587
  var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
589
588
  var EXPRESSION_CACHE_MAX = 2048;
@@ -598,7 +597,77 @@ function cacheExpression(key, factory) {
598
597
  var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
599
598
  var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
600
599
  var compiledTemplateCache = /* @__PURE__ */ new Map();
601
- var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "__proto__", "prototype"]);
600
+ var COMPILED_TEMPLATE_CACHE_MAX = 2048;
601
+ function cacheCompiledTemplate(key, template) {
602
+ if (compiledTemplateCache.size >= COMPILED_TEMPLATE_CACHE_MAX) {
603
+ const first = compiledTemplateCache.keys().next().value;
604
+ if (first !== void 0) compiledTemplateCache.delete(first);
605
+ }
606
+ compiledTemplateCache.set(key, template);
607
+ }
608
+ var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
609
+ "constructor",
610
+ "__proto__",
611
+ "prototype",
612
+ "__defineGetter__",
613
+ "__defineSetter__",
614
+ "__lookupGetter__",
615
+ "__lookupSetter__",
616
+ "eval",
617
+ "Function"
618
+ ]);
619
+ var BLOCKED_METHOD_REGEXES = (() => {
620
+ const result = [];
621
+ for (const name of UNSAFE_METHOD_NAMES) {
622
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
623
+ result.push({
624
+ name,
625
+ // Match as property access (.name) or bare identifier at start
626
+ dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
627
+ // Match bracket access with single quotes, double quotes, or backticks
628
+ bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
629
+ });
630
+ }
631
+ return result;
632
+ })();
633
+ function findBlockedMethod(expr) {
634
+ let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
635
+ cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
636
+ cleaned = cleaned.replace(/\s*\.\s*/g, ".");
637
+ for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
638
+ if (dotRe.test(cleaned)) return name;
639
+ if (bracketRe.test(cleaned)) return name;
640
+ }
641
+ if (cleaned.includes("[")) {
642
+ const bracketContents = extractBracketContents(cleaned);
643
+ for (const content of bracketContents) {
644
+ if (!content.includes("+")) continue;
645
+ const fragments = content.match(/['"`]([^'"`]*?)['"`]/g);
646
+ if (!fragments) continue;
647
+ const joined = fragments.map((f) => f.slice(1, -1)).join("");
648
+ if (UNSAFE_METHOD_NAMES.has(joined)) return joined;
649
+ }
650
+ }
651
+ return null;
652
+ }
653
+ function extractBracketContents(expr) {
654
+ const results = [];
655
+ let depth = 0;
656
+ let start = -1;
657
+ for (let i = 0; i < expr.length; i++) {
658
+ if (expr[i] === "[") {
659
+ if (depth === 0) start = i + 1;
660
+ depth++;
661
+ } else if (expr[i] === "]") {
662
+ depth--;
663
+ if (depth === 0 && start >= 0) {
664
+ results.push(expr.slice(start, i));
665
+ start = -1;
666
+ }
667
+ }
668
+ }
669
+ return results;
670
+ }
602
671
  var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
603
672
  function toTextValue(value) {
604
673
  if (value == null) return "";
@@ -873,6 +942,7 @@ function consumeStatement(raw) {
873
942
  function parseIfHandler(expr, scope) {
874
943
  const input = expr.trim();
875
944
  if (!RE_IF_PREFIX.test(input)) return null;
945
+ if (RE_EVENT_REF.test(input)) return null;
876
946
  let idx = 2;
877
947
  while (idx < input.length && /\s/.test(input[idx])) idx++;
878
948
  if (input[idx] !== "(") return null;
@@ -897,7 +967,7 @@ function parseIfHandler(expr, scope) {
897
967
  }
898
968
  if (rest.length > 0) return null;
899
969
  return (e) => {
900
- chunkVP5EOGM5_cjs.batch(() => {
970
+ chunkYSKF3VRA_cjs.batch(() => {
901
971
  if (condExpr()) thenHandler(e);
902
972
  else elseHandler?.(e);
903
973
  });
@@ -933,7 +1003,7 @@ function compileTemplate(text) {
933
1003
  dynamics,
934
1004
  hasItemRef: dynamics.length > 0
935
1005
  };
936
- compiledTemplateCache.set(text, result);
1006
+ cacheCompiledTemplate(text, result);
937
1007
  return result;
938
1008
  }
939
1009
  var templateTexts = /* @__PURE__ */ new WeakMap();
@@ -1274,6 +1344,68 @@ function cloneAttributeTemplates(el, item) {
1274
1344
  }
1275
1345
  }
1276
1346
  }
1347
+ function parseChainedAccess(expr, scope) {
1348
+ let pos = 0;
1349
+ const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
1350
+ if (!identMatch) return null;
1351
+ const rootName = identMatch[0];
1352
+ pos = rootName.length;
1353
+ if (pos >= expr.length) return null;
1354
+ if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
1355
+ const steps = [];
1356
+ while (pos < expr.length) {
1357
+ let optional = false;
1358
+ if (expr[pos] === "?" && expr[pos + 1] === ".") {
1359
+ optional = true;
1360
+ pos += 2;
1361
+ } else if (expr[pos] === ".") {
1362
+ pos += 1;
1363
+ } else {
1364
+ return null;
1365
+ }
1366
+ const nameMatch = expr.slice(pos).match(/^\w+/);
1367
+ if (!nameMatch) return null;
1368
+ const name = nameMatch[0];
1369
+ pos += name.length;
1370
+ if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
1371
+ if (pos < expr.length && expr[pos] === "(") {
1372
+ const balanced = readBalancedSegment(expr, pos, "(", ")");
1373
+ if (!balanced) return null;
1374
+ const argsRaw = balanced.inner.trim();
1375
+ const argFns = [];
1376
+ for (const arg of splitCallArgs(argsRaw)) {
1377
+ const parsed = parseExpression(arg, scope);
1378
+ if (!parsed) return null;
1379
+ argFns.push(parsed);
1380
+ }
1381
+ steps.push({ type: "call", name, optional, argFns });
1382
+ pos = balanced.end + 1;
1383
+ } else {
1384
+ steps.push({ type: "prop", name, optional });
1385
+ }
1386
+ }
1387
+ if (pos !== expr.length) return null;
1388
+ if (steps.length === 0) return null;
1389
+ const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
1390
+ return () => {
1391
+ let val = rootExpr();
1392
+ for (const step of steps) {
1393
+ if (val == null) {
1394
+ if (step.optional) return void 0;
1395
+ return void 0;
1396
+ }
1397
+ if (step.type === "prop") {
1398
+ val = val[step.name];
1399
+ } else {
1400
+ const method = val[step.name];
1401
+ if (typeof method !== "function") return void 0;
1402
+ const args = step.argFns.map((fn) => fn());
1403
+ val = method.apply(val, args);
1404
+ }
1405
+ }
1406
+ return val;
1407
+ };
1408
+ }
1277
1409
  function parseExpression(expr, scope) {
1278
1410
  const cachedFactory = expressionCache.get(expr);
1279
1411
  if (cachedFactory) return cachedFactory(scope);
@@ -1324,24 +1456,9 @@ function parseExpressionUncached(expr, scope) {
1324
1456
  if (RE_IDENTIFIER.test(expr)) {
1325
1457
  return () => scope.getters[expr]?.();
1326
1458
  }
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
- };
1459
+ {
1460
+ const chainResult = parseChainedAccess(expr, scope);
1461
+ if (chainResult) return chainResult;
1345
1462
  }
1346
1463
  const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
1347
1464
  if (groupedCallMatch) {
@@ -1365,28 +1482,6 @@ function parseExpressionUncached(expr, scope) {
1365
1482
  return method.apply(base, args);
1366
1483
  };
1367
1484
  }
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
1485
  if (expr.startsWith("!")) {
1391
1486
  const inner = parseExpression(expr.slice(1).trim(), scope);
1392
1487
  if (inner) return () => !inner();
@@ -1405,6 +1500,29 @@ function parseExpressionUncached(expr, scope) {
1405
1500
  return () => objExpr()?.[key];
1406
1501
  }
1407
1502
  }
1503
+ if (expr.startsWith("[")) {
1504
+ const balanced = readBalancedSegment(expr, 0, "[", "]");
1505
+ if (balanced && balanced.end === expr.length - 1) {
1506
+ const inner = balanced.inner.trim();
1507
+ if (inner === "") {
1508
+ return () => [];
1509
+ }
1510
+ const elements = splitCallArgs(inner);
1511
+ const elementFns = [];
1512
+ let allParsed = true;
1513
+ for (const el of elements) {
1514
+ const parsed = parseExpression(el.trim(), scope);
1515
+ if (!parsed) {
1516
+ allParsed = false;
1517
+ break;
1518
+ }
1519
+ elementFns.push(parsed);
1520
+ }
1521
+ if (allParsed) {
1522
+ return () => elementFns.map((fn) => fn());
1523
+ }
1524
+ }
1525
+ }
1408
1526
  const ternaryMatch = expr.match(RE_TERNARY);
1409
1527
  if (ternaryMatch) {
1410
1528
  const cond = parseExpression(ternaryMatch[1].trim(), scope);
@@ -1534,6 +1652,15 @@ function getScopeCache(cache, scope) {
1534
1652
  }
1535
1653
  return scoped;
1536
1654
  }
1655
+ function cspExpressionHint(expr) {
1656
+ if (expr.includes("...")) {
1657
+ return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
1658
+ }
1659
+ if (expr.includes("=>")) {
1660
+ return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
1661
+ }
1662
+ return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
1663
+ }
1537
1664
  function buildEvaluator(expr, scope) {
1538
1665
  const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1539
1666
  const cache = getScopeCache(scopeExpressionCache, scope);
@@ -1546,11 +1673,17 @@ function buildEvaluator(expr, scope) {
1546
1673
  }
1547
1674
  if (!_allowUnsafeEval) {
1548
1675
  dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
1549
- reportDiagnostic("expression-unsupported", cleaned, "Unsupported expression in CSP-safe mode");
1676
+ reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
1550
1677
  const blocked = () => void 0;
1551
1678
  cache.set(cleaned, blocked);
1552
1679
  return blocked;
1553
1680
  }
1681
+ const blockedMethod = findBlockedMethod(cleaned);
1682
+ if (blockedMethod) {
1683
+ const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
1684
+ reportDiagnostic("expression-unsupported", cleaned, msg);
1685
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1686
+ }
1554
1687
  try {
1555
1688
  const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
1556
1689
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1558,6 +1691,7 @@ function buildEvaluator(expr, scope) {
1558
1691
  return key in scope.getters;
1559
1692
  },
1560
1693
  get(_, key) {
1694
+ if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
1561
1695
  const g = scope.getters[key];
1562
1696
  return g ? g() : void 0;
1563
1697
  }
@@ -1566,7 +1700,7 @@ function buildEvaluator(expr, scope) {
1566
1700
  cache.set(cleaned, unsafe);
1567
1701
  return unsafe;
1568
1702
  } catch {
1569
- reportDiagnostic("expression-unsupported", cleaned, "Expression failed to compile");
1703
+ 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
1704
  const failed = () => void 0;
1571
1705
  cache.set(cleaned, failed);
1572
1706
  return failed;
@@ -1582,7 +1716,7 @@ function parseHandler(expr, scope) {
1582
1716
  const handlers = stmts.map((s) => parseHandler(s, scope));
1583
1717
  if (handlers.every((h) => h !== null)) {
1584
1718
  return (e) => {
1585
- chunkVP5EOGM5_cjs.batch(() => {
1719
+ chunkYSKF3VRA_cjs.batch(() => {
1586
1720
  for (const h of handlers) h(e);
1587
1721
  });
1588
1722
  };
@@ -1595,7 +1729,7 @@ function parseHandler(expr, scope) {
1595
1729
  const name = incrMatch[1];
1596
1730
  const op = incrMatch[2];
1597
1731
  return () => {
1598
- chunkVP5EOGM5_cjs.batch(() => {
1732
+ chunkYSKF3VRA_cjs.batch(() => {
1599
1733
  const val = scope.getters[name]?.() ?? 0;
1600
1734
  scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
1601
1735
  });
@@ -1606,7 +1740,7 @@ function parseHandler(expr, scope) {
1606
1740
  const op = preIncrMatch[1];
1607
1741
  const name = preIncrMatch[2];
1608
1742
  return () => {
1609
- chunkVP5EOGM5_cjs.batch(() => {
1743
+ chunkYSKF3VRA_cjs.batch(() => {
1610
1744
  const val = scope.getters[name]?.() ?? 0;
1611
1745
  scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
1612
1746
  });
@@ -1616,7 +1750,7 @@ function parseHandler(expr, scope) {
1616
1750
  if (toggleMatch && toggleMatch[1] === toggleMatch[2]) {
1617
1751
  const name = toggleMatch[1];
1618
1752
  return () => {
1619
- chunkVP5EOGM5_cjs.batch(() => {
1753
+ chunkYSKF3VRA_cjs.batch(() => {
1620
1754
  scope.setters[name]?.(!scope.getters[name]?.());
1621
1755
  });
1622
1756
  };
@@ -1628,7 +1762,7 @@ function parseHandler(expr, scope) {
1628
1762
  if (valExpr) {
1629
1763
  if (_debug) dbg(`parseHandler: assignment "${name} = ..." \u2014 setter exists:`, !!scope.setters[name], ", getter exists:", !!scope.getters[name]);
1630
1764
  return () => {
1631
- chunkVP5EOGM5_cjs.batch(() => {
1765
+ chunkYSKF3VRA_cjs.batch(() => {
1632
1766
  const val = valExpr();
1633
1767
  if (_debug) dbg(`SETTER: ${name} = ${val} (was: ${scope.getters[name]?.()})`);
1634
1768
  scope.setters[name]?.(val);
@@ -1643,7 +1777,7 @@ function parseHandler(expr, scope) {
1643
1777
  const valExpr = parseExpression(compoundMatch[3].trim(), scope);
1644
1778
  if (valExpr) {
1645
1779
  return () => {
1646
- chunkVP5EOGM5_cjs.batch(() => {
1780
+ chunkYSKF3VRA_cjs.batch(() => {
1647
1781
  const current = scope.getters[name]?.() ?? 0;
1648
1782
  const val = valExpr();
1649
1783
  switch (op) {
@@ -1672,7 +1806,13 @@ function parseHandler(expr, scope) {
1672
1806
  return null;
1673
1807
  }
1674
1808
  function buildHandler(expr, scope) {
1675
- const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1809
+ let cleaned = expr.trim();
1810
+ if (cleaned.startsWith("{")) {
1811
+ const seg = readBalancedSegment(cleaned, 0, "{", "}");
1812
+ if (seg && seg.end === cleaned.length - 1) {
1813
+ cleaned = seg.inner.trim();
1814
+ }
1815
+ }
1676
1816
  const cache = getScopeCache(scopeHandlerCache, scope);
1677
1817
  const cached = cache.get(cleaned);
1678
1818
  if (cached) return cached;
@@ -1684,7 +1824,7 @@ function buildHandler(expr, scope) {
1684
1824
  }
1685
1825
  if (!_allowUnsafeEval) {
1686
1826
  dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
1687
- reportDiagnostic("handler-unsupported", cleaned, "Unsupported handler in CSP-safe mode");
1827
+ reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
1688
1828
  const result = {
1689
1829
  handler: () => {
1690
1830
  },
@@ -1693,6 +1833,12 @@ function buildHandler(expr, scope) {
1693
1833
  cache.set(cleaned, result);
1694
1834
  return result;
1695
1835
  }
1836
+ const blockedMethod = findBlockedMethod(cleaned);
1837
+ if (blockedMethod) {
1838
+ const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
1839
+ reportDiagnostic("handler-unsupported", cleaned, msg);
1840
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1841
+ }
1696
1842
  try {
1697
1843
  const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
1698
1844
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1701,6 +1847,7 @@ function buildHandler(expr, scope) {
1701
1847
  return key in scope.getters || key in scope.setters;
1702
1848
  },
1703
1849
  get(_, key) {
1850
+ if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
1704
1851
  const g = scope.getters[key];
1705
1852
  return g ? g() : void 0;
1706
1853
  },
@@ -1711,7 +1858,7 @@ function buildHandler(expr, scope) {
1711
1858
  }
1712
1859
  });
1713
1860
  const unsafeHandler = (e) => {
1714
- chunkVP5EOGM5_cjs.batch(() => fn(proxy, e, e));
1861
+ chunkYSKF3VRA_cjs.batch(() => fn(proxy, e, e));
1715
1862
  };
1716
1863
  const result = {
1717
1864
  handler: unsafeHandler,
@@ -1720,7 +1867,7 @@ function buildHandler(expr, scope) {
1720
1867
  cache.set(cleaned, result);
1721
1868
  return result;
1722
1869
  } catch {
1723
- reportDiagnostic("handler-unsupported", cleaned, "Handler failed to compile");
1870
+ 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
1871
  const result = {
1725
1872
  handler: () => {
1726
1873
  },
@@ -1730,16 +1877,21 @@ function buildHandler(expr, scope) {
1730
1877
  return result;
1731
1878
  }
1732
1879
  }
1880
+ var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1733
1881
  function parseState(raw) {
1882
+ let parsed;
1734
1883
  try {
1735
- return JSON.parse(raw);
1884
+ parsed = JSON.parse(raw);
1736
1885
  } catch {
1737
- try {
1738
- return JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
1739
- } catch {
1740
- return {};
1886
+ if (_debug) {
1887
+ dbg("parseState: Invalid JSON in data-forma-state \u2014 use valid JSON with quoted keys. Got:", raw.slice(0, 200));
1741
1888
  }
1889
+ return {};
1890
+ }
1891
+ for (const key of FORBIDDEN_STATE_KEYS) {
1892
+ if (key in parsed) delete parsed[key];
1742
1893
  }
1894
+ return parsed;
1743
1895
  }
1744
1896
  function initScope(stateEl) {
1745
1897
  const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
@@ -1754,7 +1906,7 @@ function initScope(stateEl) {
1754
1906
  const getters = {};
1755
1907
  const setters = {};
1756
1908
  for (const [key, initial] of Object.entries(state)) {
1757
- const [get, set] = chunkFPSLC62A_cjs.createValueSignal(initial);
1909
+ const [get, set] = chunkKH3F5NRU_cjs.createSignal(initial);
1758
1910
  getters[key] = get;
1759
1911
  setters[key] = set;
1760
1912
  }
@@ -1762,6 +1914,18 @@ function initScope(stateEl) {
1762
1914
  return { getters, setters };
1763
1915
  }
1764
1916
  function bindElement(el, scope, disposers) {
1917
+ const elMagics = {
1918
+ $el: el,
1919
+ $dispatch: (name, detail) => {
1920
+ el.dispatchEvent(new CustomEvent(name, {
1921
+ bubbles: true,
1922
+ composed: true,
1923
+ // crosses Shadow DOM boundaries (important for <forma-stage>)
1924
+ detail
1925
+ }));
1926
+ }
1927
+ };
1928
+ scope = createChildScope(scope, elMagics);
1765
1929
  const known = getDirectives(el);
1766
1930
  const computedAttr = !known || known.has("data-computed") ? el.getAttribute("data-computed") : null;
1767
1931
  if (computedAttr) {
@@ -1776,7 +1940,7 @@ function bindElement(el, scope, disposers) {
1776
1940
  const prevGetter = scope.getters[name];
1777
1941
  delete scope.getters[name];
1778
1942
  const evaluate = buildEvaluator(`{${expr}}`, scope);
1779
- const getter = chunkFPSLC62A_cjs.createComputed(evaluate);
1943
+ const getter = chunkKH3F5NRU_cjs.createComputed(evaluate);
1780
1944
  scope.getters[name] = getter;
1781
1945
  if (!prevGetter) {
1782
1946
  delete scope.setters[name];
@@ -1787,7 +1951,7 @@ function bindElement(el, scope, disposers) {
1787
1951
  const textExpr = !known || known.has("data-text") ? el.getAttribute("data-text") : null;
1788
1952
  if (textExpr) {
1789
1953
  const evaluate = buildEvaluator(textExpr, scope);
1790
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
1954
+ const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
1791
1955
  setElementTextFast(el, toTextValue(evaluate()));
1792
1956
  });
1793
1957
  disposers.push(dispose);
@@ -1802,7 +1966,7 @@ function bindElement(el, scope, disposers) {
1802
1966
  dbg(`bindElement: data-show="${showExpr}" on <${tag}${cls}>`);
1803
1967
  }
1804
1968
  let initialized = false;
1805
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
1969
+ const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
1806
1970
  const visible = !!evaluate();
1807
1971
  if (_debug) dbg(`data-show effect: "${showExpr}" \u2192 ${visible}`);
1808
1972
  applyShowVisibility(el, visible, transition, !initialized);
@@ -1821,7 +1985,7 @@ function bindElement(el, scope, disposers) {
1821
1985
  const parent = el.parentNode;
1822
1986
  let inserted = true;
1823
1987
  let initialized = false;
1824
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
1988
+ const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
1825
1989
  const show = !!evaluate();
1826
1990
  if (show && !inserted) {
1827
1991
  clearTransitionState(el);
@@ -1860,7 +2024,7 @@ function bindElement(el, scope, disposers) {
1860
2024
  const setter = scope.setters[prop];
1861
2025
  if (getter && setter) {
1862
2026
  const input = el;
1863
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2027
+ const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
1864
2028
  const val = getter();
1865
2029
  if (input.type === "checkbox") {
1866
2030
  input.checked = !!val;
@@ -1924,14 +2088,14 @@ function bindElement(el, scope, disposers) {
1924
2088
  } else if (name.startsWith("data-class:")) {
1925
2089
  const cls = name.slice(11);
1926
2090
  const evaluate = buildEvaluator(attr.value, scope);
1927
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2091
+ const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
1928
2092
  el.classList.toggle(cls, !!evaluate());
1929
2093
  });
1930
2094
  disposers.push(dispose);
1931
2095
  } else if (name.startsWith("data-bind:")) {
1932
2096
  const attrName = name.slice(10);
1933
2097
  const evaluate = buildEvaluator(attr.value, scope);
1934
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2098
+ const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
1935
2099
  const val = evaluate();
1936
2100
  if (val == null || val === false) {
1937
2101
  el.removeAttribute(attrName);
@@ -1954,7 +2118,7 @@ function bindElement(el, scope, disposers) {
1954
2118
  if (saved !== null) setter(JSON.parse(saved));
1955
2119
  } catch {
1956
2120
  }
1957
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2121
+ const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
1958
2122
  try {
1959
2123
  localStorage.setItem(key, JSON.stringify(getter()));
1960
2124
  } catch {
@@ -2041,7 +2205,7 @@ function bindElement(el, scope, disposers) {
2041
2205
  });
2042
2206
  }
2043
2207
  } : void 0;
2044
- const dispose = chunkVP5EOGM5_cjs.internalEffect(() => {
2208
+ const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
2045
2209
  const rawItems = evaluate();
2046
2210
  if (!Array.isArray(rawItems)) {
2047
2211
  for (const n of oldNodes) {
@@ -2062,7 +2226,7 @@ function bindElement(el, scope, disposers) {
2062
2226
  }
2063
2227
  const prevNodes = new Set(oldNodes);
2064
2228
  if (keyProp) {
2065
- const result = chunkVP5EOGM5_cjs.reconcileList(
2229
+ const result = chunkYSKF3VRA_cjs.reconcileList(
2066
2230
  el,
2067
2231
  oldItems,
2068
2232
  rawItems,
@@ -2092,7 +2256,7 @@ function bindElement(el, scope, disposers) {
2092
2256
  } else {
2093
2257
  const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
2094
2258
  const oldWrapped = oldItems;
2095
- const result = chunkVP5EOGM5_cjs.reconcileList(
2259
+ const result = chunkYSKF3VRA_cjs.reconcileList(
2096
2260
  el,
2097
2261
  oldWrapped,
2098
2262
  wrapped,
@@ -2141,16 +2305,16 @@ function bindElement(el, scope, disposers) {
2141
2305
  else if (k === "error") errorTarget = v;
2142
2306
  else if (k === "poll") interval = parseInt(v ?? "0", 10);
2143
2307
  }
2144
- const [getTarget, setTarget] = chunkFPSLC62A_cjs.createValueSignal(null);
2308
+ const [getTarget, setTarget] = chunkKH3F5NRU_cjs.createSignal(null);
2145
2309
  scope.getters[target] = getTarget;
2146
2310
  scope.setters[target] = setTarget;
2147
2311
  if (loadingTarget) {
2148
- const [gl, sl] = chunkFPSLC62A_cjs.createValueSignal(false);
2312
+ const [gl, sl] = chunkKH3F5NRU_cjs.createSignal(false);
2149
2313
  scope.getters[loadingTarget] = gl;
2150
2314
  scope.setters[loadingTarget] = sl;
2151
2315
  }
2152
2316
  if (errorTarget) {
2153
- const [ge, se] = chunkFPSLC62A_cjs.createValueSignal(null);
2317
+ const [ge, se] = chunkKH3F5NRU_cjs.createSignal(null);
2154
2318
  scope.getters[errorTarget] = ge;
2155
2319
  scope.setters[errorTarget] = se;
2156
2320
  }
@@ -2409,6 +2573,7 @@ function setUnsafeEvalMode(mode) {
2409
2573
  _unsafeEvalMode = mode;
2410
2574
  if (mode === "locked-off") _allowUnsafeEval = false;
2411
2575
  if (mode === "locked-on") _allowUnsafeEval = true;
2576
+ if (mode === "mutable") _allowUnsafeEval = true;
2412
2577
  scopeExpressionCache = /* @__PURE__ */ new WeakMap();
2413
2578
  scopeHandlerCache = /* @__PURE__ */ new WeakMap();
2414
2579
  }
@@ -2461,7 +2626,7 @@ function getScopes() {
2461
2626
  function setScopeValue(element, key, value) {
2462
2627
  const scope = element.__formaScope;
2463
2628
  if (!scope?.setters[key]) return;
2464
- chunkVP5EOGM5_cjs.batch(() => {
2629
+ chunkYSKF3VRA_cjs.batch(() => {
2465
2630
  scope.setters[key](value);
2466
2631
  });
2467
2632
  }
@@ -2470,7 +2635,7 @@ function resetScope(element) {
2470
2635
  const initialJSON = element.__formaInitialState;
2471
2636
  if (!scope || !initialJSON) return;
2472
2637
  const initial = parseState(initialJSON);
2473
- chunkVP5EOGM5_cjs.batch(() => {
2638
+ chunkYSKF3VRA_cjs.batch(() => {
2474
2639
  for (const [key, val] of Object.entries(initial)) {
2475
2640
  scope.setters[key]?.(val);
2476
2641
  }
@@ -2500,7 +2665,7 @@ function getReconciler() {
2500
2665
  }
2501
2666
  }
2502
2667
  },
2503
- batch: chunkVP5EOGM5_cjs.batch
2668
+ batch: chunkYSKF3VRA_cjs.batch
2504
2669
  });
2505
2670
  }
2506
2671
  return _reconciler;