@getforma/core 0.3.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 +19 -5
  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 +24 -3
  37. package/dist/chunk-VKKPX4YU.js.map +0 -1
  38. package/dist/chunk-VP5EOGM5.cjs.map +0 -1
@@ -0,0 +1,86 @@
1
+ type UnsafeEvalMode = 'mutable' | 'locked-off' | 'locked-on';
2
+ interface RuntimeDiagnostic {
3
+ kind: 'handler-unsupported' | 'expression-unsupported';
4
+ expr: string;
5
+ reason: string;
6
+ count: number;
7
+ firstSeenAt: number;
8
+ lastSeenAt: number;
9
+ }
10
+ /** Yield control to keep the main thread responsive during large batches. */
11
+ declare function yieldToMain(): Promise<void>;
12
+ interface ContainmentHintsOptions {
13
+ selector?: string;
14
+ contain?: string;
15
+ contentVisibility?: string;
16
+ containIntrinsicSize?: string;
17
+ skipIfAlreadySet?: boolean;
18
+ }
19
+ /** Apply CSS containment hints to opt-in containers for lower layout/paint cost. */
20
+ declare function applyContainmentHints(root?: ParentNode, options?: ContainmentHintsOptions): number;
21
+ /**
22
+ * Load a pre-compiled directive map from the server.
23
+ * Keys are data-forma-id values, values are arrays of directive attribute names.
24
+ */
25
+ declare function setDirectiveMap(map: Record<string, string[]> | null): void;
26
+ declare function initRuntime(): void;
27
+ /** Dispose all FormaJS scopes — clears effects, intervals, event listeners, and stops the observer. */
28
+ declare function destroyRuntime(): void;
29
+ /**
30
+ * Mount a specific element or subtree — scans for `data-forma-state`
31
+ * elements and initializes their reactive bindings.
32
+ *
33
+ * Use this for manual control when injecting HTML dynamically.
34
+ * With the MutationObserver active, this is usually not needed.
35
+ *
36
+ * @param el - The root element to scan (checks itself and all descendants).
37
+ */
38
+ declare function mount(el: Element): void;
39
+ /**
40
+ * Unmount a specific element or subtree — disposes all reactive bindings
41
+ * for `data-forma-state` elements within.
42
+ *
43
+ * @param el - The root element to clean up (checks itself and all descendants).
44
+ */
45
+ declare function unmount(el: Element): void;
46
+ /** Enable/disable debug logging. Also toggleable via window.__FORMA_DEBUG = true */
47
+ declare function setDebug(on: boolean): void;
48
+ /** Set unsafe-eval mode. `locked-off` is hardened and non-toggleable via setUnsafeEval. */
49
+ declare function setUnsafeEvalMode(mode: UnsafeEvalMode): void;
50
+ /** Enable/disable unsafe `new Function` fallback for complex expressions. */
51
+ declare function setUnsafeEval(on: boolean): void;
52
+ /** Get current unsafe-eval policy mode. */
53
+ declare function getUnsafeEvalMode(): UnsafeEvalMode;
54
+ /** Enable/disable runtime diagnostics for unsupported expressions/handlers. */
55
+ declare function setDiagnostics(on: boolean): void;
56
+ /** Runtime diagnostics captured while parsing/binding templates. */
57
+ declare function getDiagnostics(): RuntimeDiagnostic[];
58
+ /** Clear runtime diagnostics collected so far. */
59
+ declare function clearDiagnostics(): void;
60
+ interface ScopeDescriptor {
61
+ element: Element;
62
+ id: string;
63
+ values: Record<string, {
64
+ value: unknown;
65
+ type: string;
66
+ }>;
67
+ initialJSON: string;
68
+ }
69
+ /**
70
+ * DevTools: enumerate all active scopes and their current signal values.
71
+ * Only called when the State Inspector panel is open — zero overhead otherwise.
72
+ */
73
+ declare function getScopes(): ScopeDescriptor[];
74
+ /**
75
+ * DevTools: set a state value on a specific scope element.
76
+ * Triggers normal reactive effects (data-show, data-text, etc.).
77
+ */
78
+ declare function setScopeValue(element: Element, key: string, value: unknown): void;
79
+ /**
80
+ * DevTools: reset all values on a scope to their initial JSON state.
81
+ */
82
+ declare function resetScope(element: Element): void;
83
+ /** Reconcile a container's DOM against a new HTML string. */
84
+ declare function reconcile(container: Element, html: string): void;
85
+
86
+ export { applyContainmentHints, clearDiagnostics, destroyRuntime, getDiagnostics, getScopes, getUnsafeEvalMode, initRuntime, mount, reconcile, resetScope, setDebug, setDiagnostics, setDirectiveMap, setScopeValue, setUnsafeEval, setUnsafeEvalMode, unmount, yieldToMain };
@@ -0,0 +1,86 @@
1
+ type UnsafeEvalMode = 'mutable' | 'locked-off' | 'locked-on';
2
+ interface RuntimeDiagnostic {
3
+ kind: 'handler-unsupported' | 'expression-unsupported';
4
+ expr: string;
5
+ reason: string;
6
+ count: number;
7
+ firstSeenAt: number;
8
+ lastSeenAt: number;
9
+ }
10
+ /** Yield control to keep the main thread responsive during large batches. */
11
+ declare function yieldToMain(): Promise<void>;
12
+ interface ContainmentHintsOptions {
13
+ selector?: string;
14
+ contain?: string;
15
+ contentVisibility?: string;
16
+ containIntrinsicSize?: string;
17
+ skipIfAlreadySet?: boolean;
18
+ }
19
+ /** Apply CSS containment hints to opt-in containers for lower layout/paint cost. */
20
+ declare function applyContainmentHints(root?: ParentNode, options?: ContainmentHintsOptions): number;
21
+ /**
22
+ * Load a pre-compiled directive map from the server.
23
+ * Keys are data-forma-id values, values are arrays of directive attribute names.
24
+ */
25
+ declare function setDirectiveMap(map: Record<string, string[]> | null): void;
26
+ declare function initRuntime(): void;
27
+ /** Dispose all FormaJS scopes — clears effects, intervals, event listeners, and stops the observer. */
28
+ declare function destroyRuntime(): void;
29
+ /**
30
+ * Mount a specific element or subtree — scans for `data-forma-state`
31
+ * elements and initializes their reactive bindings.
32
+ *
33
+ * Use this for manual control when injecting HTML dynamically.
34
+ * With the MutationObserver active, this is usually not needed.
35
+ *
36
+ * @param el - The root element to scan (checks itself and all descendants).
37
+ */
38
+ declare function mount(el: Element): void;
39
+ /**
40
+ * Unmount a specific element or subtree — disposes all reactive bindings
41
+ * for `data-forma-state` elements within.
42
+ *
43
+ * @param el - The root element to clean up (checks itself and all descendants).
44
+ */
45
+ declare function unmount(el: Element): void;
46
+ /** Enable/disable debug logging. Also toggleable via window.__FORMA_DEBUG = true */
47
+ declare function setDebug(on: boolean): void;
48
+ /** Set unsafe-eval mode. `locked-off` is hardened and non-toggleable via setUnsafeEval. */
49
+ declare function setUnsafeEvalMode(mode: UnsafeEvalMode): void;
50
+ /** Enable/disable unsafe `new Function` fallback for complex expressions. */
51
+ declare function setUnsafeEval(on: boolean): void;
52
+ /** Get current unsafe-eval policy mode. */
53
+ declare function getUnsafeEvalMode(): UnsafeEvalMode;
54
+ /** Enable/disable runtime diagnostics for unsupported expressions/handlers. */
55
+ declare function setDiagnostics(on: boolean): void;
56
+ /** Runtime diagnostics captured while parsing/binding templates. */
57
+ declare function getDiagnostics(): RuntimeDiagnostic[];
58
+ /** Clear runtime diagnostics collected so far. */
59
+ declare function clearDiagnostics(): void;
60
+ interface ScopeDescriptor {
61
+ element: Element;
62
+ id: string;
63
+ values: Record<string, {
64
+ value: unknown;
65
+ type: string;
66
+ }>;
67
+ initialJSON: string;
68
+ }
69
+ /**
70
+ * DevTools: enumerate all active scopes and their current signal values.
71
+ * Only called when the State Inspector panel is open — zero overhead otherwise.
72
+ */
73
+ declare function getScopes(): ScopeDescriptor[];
74
+ /**
75
+ * DevTools: set a state value on a specific scope element.
76
+ * Triggers normal reactive effects (data-show, data-text, etc.).
77
+ */
78
+ declare function setScopeValue(element: Element, key: string, value: unknown): void;
79
+ /**
80
+ * DevTools: reset all values on a scope to their initial JSON state.
81
+ */
82
+ declare function resetScope(element: Element): void;
83
+ /** Reconcile a container's DOM against a new HTML string. */
84
+ declare function reconcile(container: Element, html: string): void;
85
+
86
+ export { applyContainmentHints, clearDiagnostics, destroyRuntime, getDiagnostics, getScopes, getUnsafeEvalMode, initRuntime, mount, reconcile, resetScope, setDebug, setDiagnostics, setDirectiveMap, setScopeValue, setUnsafeEval, setUnsafeEvalMode, unmount, yieldToMain };
@@ -753,6 +753,7 @@ if (buildUnsafeEvalMode) {
753
753
  _unsafeEvalMode = buildUnsafeEvalMode;
754
754
  if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
755
755
  if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
756
+ if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
756
757
  }
757
758
  var runtimeConfig = readRuntimeConfig();
758
759
  var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
@@ -830,7 +831,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
830
831
  var RE_NUMBER = /^-?\d+(\.\d+)?$/;
831
832
  var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
832
833
  var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
833
- var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
834
834
  var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
835
835
  var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
836
836
  var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
@@ -841,7 +841,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
841
841
  var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
842
842
  var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
843
843
  var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
844
- var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
845
844
  var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
846
845
  var RE_STRIP_BRACES = /^\{|\}$/g;
847
846
  var RE_DIGIT_ONLY = /^\d+$/;
@@ -856,6 +855,7 @@ var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
856
855
  var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
857
856
  var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
858
857
  var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
858
+ var RE_EVENT_REF = /\bevent\b|\$event\b/;
859
859
  var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
860
860
  var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
861
861
  var EXPRESSION_CACHE_MAX = 2048;
@@ -870,7 +870,40 @@ function cacheExpression(key, factory) {
870
870
  var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
871
871
  var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
872
872
  var compiledTemplateCache = /* @__PURE__ */ new Map();
873
- var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "__proto__", "prototype"]);
873
+ var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
874
+ "constructor",
875
+ "__proto__",
876
+ "prototype",
877
+ "__defineGetter__",
878
+ "__defineSetter__",
879
+ "__lookupGetter__",
880
+ "__lookupSetter__",
881
+ "eval"
882
+ ]);
883
+ var BLOCKED_METHOD_REGEXES = (() => {
884
+ const result = [];
885
+ for (const name of UNSAFE_METHOD_NAMES) {
886
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
887
+ result.push({
888
+ name,
889
+ // Match as property access (.name) or bare identifier at start
890
+ dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
891
+ // Match bracket access with single quotes, double quotes, or backticks
892
+ bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
893
+ });
894
+ }
895
+ return result;
896
+ })();
897
+ function findBlockedMethod(expr) {
898
+ let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
899
+ cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
900
+ cleaned = cleaned.replace(/\s*\.\s*/g, ".");
901
+ for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
902
+ if (dotRe.test(cleaned)) return name;
903
+ if (bracketRe.test(cleaned)) return name;
904
+ }
905
+ return null;
906
+ }
874
907
  var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
875
908
  function toTextValue(value2) {
876
909
  if (value2 == null) return "";
@@ -1145,6 +1178,7 @@ function consumeStatement(raw) {
1145
1178
  function parseIfHandler(expr, scope) {
1146
1179
  const input = expr.trim();
1147
1180
  if (!RE_IF_PREFIX.test(input)) return null;
1181
+ if (RE_EVENT_REF.test(input)) return null;
1148
1182
  let idx = 2;
1149
1183
  while (idx < input.length && /\s/.test(input[idx])) idx++;
1150
1184
  if (input[idx] !== "(") return null;
@@ -1546,6 +1580,68 @@ function cloneAttributeTemplates(el, item) {
1546
1580
  }
1547
1581
  }
1548
1582
  }
1583
+ function parseChainedAccess(expr, scope) {
1584
+ let pos = 0;
1585
+ const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
1586
+ if (!identMatch) return null;
1587
+ const rootName = identMatch[0];
1588
+ pos = rootName.length;
1589
+ if (pos >= expr.length) return null;
1590
+ if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
1591
+ const steps = [];
1592
+ while (pos < expr.length) {
1593
+ let optional = false;
1594
+ if (expr[pos] === "?" && expr[pos + 1] === ".") {
1595
+ optional = true;
1596
+ pos += 2;
1597
+ } else if (expr[pos] === ".") {
1598
+ pos += 1;
1599
+ } else {
1600
+ return null;
1601
+ }
1602
+ const nameMatch = expr.slice(pos).match(/^\w+/);
1603
+ if (!nameMatch) return null;
1604
+ const name = nameMatch[0];
1605
+ pos += name.length;
1606
+ if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
1607
+ if (pos < expr.length && expr[pos] === "(") {
1608
+ const balanced = readBalancedSegment(expr, pos, "(", ")");
1609
+ if (!balanced) return null;
1610
+ const argsRaw = balanced.inner.trim();
1611
+ const argFns = [];
1612
+ for (const arg of splitCallArgs(argsRaw)) {
1613
+ const parsed = parseExpression(arg, scope);
1614
+ if (!parsed) return null;
1615
+ argFns.push(parsed);
1616
+ }
1617
+ steps.push({ type: "call", name, optional, argFns });
1618
+ pos = balanced.end + 1;
1619
+ } else {
1620
+ steps.push({ type: "prop", name, optional });
1621
+ }
1622
+ }
1623
+ if (pos !== expr.length) return null;
1624
+ if (steps.length === 0) return null;
1625
+ const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
1626
+ return () => {
1627
+ let val = rootExpr();
1628
+ for (const step of steps) {
1629
+ if (val == null) {
1630
+ if (step.optional) return void 0;
1631
+ return void 0;
1632
+ }
1633
+ if (step.type === "prop") {
1634
+ val = val[step.name];
1635
+ } else {
1636
+ const method = val[step.name];
1637
+ if (typeof method !== "function") return void 0;
1638
+ const args = step.argFns.map((fn) => fn());
1639
+ val = method.apply(val, args);
1640
+ }
1641
+ }
1642
+ return val;
1643
+ };
1644
+ }
1549
1645
  function parseExpression(expr, scope) {
1550
1646
  const cachedFactory = expressionCache.get(expr);
1551
1647
  if (cachedFactory) return cachedFactory(scope);
@@ -1596,24 +1692,9 @@ function parseExpressionUncached(expr, scope) {
1596
1692
  if (RE_IDENTIFIER.test(expr)) {
1597
1693
  return () => scope.getters[expr]?.();
1598
1694
  }
1599
- const dotMatch = expr.match(RE_DOT_ACCESS);
1600
- if (dotMatch) {
1601
- const p1 = dotMatch[1], p2 = dotMatch[2];
1602
- return () => {
1603
- const obj = scope.getters[p1]?.();
1604
- return obj?.[p2];
1605
- };
1606
- }
1607
- const deepDotMatch = expr.match(RE_DEEP_DOT);
1608
- if (deepDotMatch) {
1609
- const p1 = deepDotMatch[1], p2 = deepDotMatch[2], p3 = deepDotMatch[3], p4 = deepDotMatch[4];
1610
- return () => {
1611
- let val = scope.getters[p1]?.();
1612
- val = val?.[p2];
1613
- val = val?.[p3];
1614
- if (p4) val = val?.[p4];
1615
- return val;
1616
- };
1695
+ {
1696
+ const chainResult = parseChainedAccess(expr, scope);
1697
+ if (chainResult) return chainResult;
1617
1698
  }
1618
1699
  const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
1619
1700
  if (groupedCallMatch) {
@@ -1637,28 +1718,6 @@ function parseExpressionUncached(expr, scope) {
1637
1718
  return method.apply(base, args);
1638
1719
  };
1639
1720
  }
1640
- const callMatch = expr.match(RE_METHOD_CALL);
1641
- if (callMatch) {
1642
- const baseName = callMatch[1];
1643
- const methodName = callMatch[2];
1644
- const argsRaw = callMatch[3].trim();
1645
- if (UNSAFE_METHOD_NAMES.has(methodName)) return () => void 0;
1646
- const baseExpr = baseName === "Math" ? (() => Math) : parseExpression(baseName, scope);
1647
- if (!baseExpr) return null;
1648
- const argFns = [];
1649
- for (const arg of splitCallArgs(argsRaw)) {
1650
- const parsed = parseExpression(arg, scope);
1651
- if (!parsed) return null;
1652
- argFns.push(parsed);
1653
- }
1654
- return () => {
1655
- const base = baseExpr();
1656
- const method = base?.[methodName];
1657
- if (typeof method !== "function") return void 0;
1658
- const args = argFns.map((fn) => fn());
1659
- return method.apply(base, args);
1660
- };
1661
- }
1662
1721
  if (expr.startsWith("!")) {
1663
1722
  const inner = parseExpression(expr.slice(1).trim(), scope);
1664
1723
  if (inner) return () => !inner();
@@ -1677,6 +1736,29 @@ function parseExpressionUncached(expr, scope) {
1677
1736
  return () => objExpr()?.[key];
1678
1737
  }
1679
1738
  }
1739
+ if (expr.startsWith("[")) {
1740
+ const balanced = readBalancedSegment(expr, 0, "[", "]");
1741
+ if (balanced && balanced.end === expr.length - 1) {
1742
+ const inner = balanced.inner.trim();
1743
+ if (inner === "") {
1744
+ return () => [];
1745
+ }
1746
+ const elements = splitCallArgs(inner);
1747
+ const elementFns = [];
1748
+ let allParsed = true;
1749
+ for (const el of elements) {
1750
+ const parsed = parseExpression(el.trim(), scope);
1751
+ if (!parsed) {
1752
+ allParsed = false;
1753
+ break;
1754
+ }
1755
+ elementFns.push(parsed);
1756
+ }
1757
+ if (allParsed) {
1758
+ return () => elementFns.map((fn) => fn());
1759
+ }
1760
+ }
1761
+ }
1680
1762
  const ternaryMatch = expr.match(RE_TERNARY);
1681
1763
  if (ternaryMatch) {
1682
1764
  const cond = parseExpression(ternaryMatch[1].trim(), scope);
@@ -1806,6 +1888,15 @@ function getScopeCache(cache, scope) {
1806
1888
  }
1807
1889
  return scoped;
1808
1890
  }
1891
+ function cspExpressionHint(expr) {
1892
+ if (expr.includes("...")) {
1893
+ return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
1894
+ }
1895
+ if (expr.includes("=>")) {
1896
+ return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
1897
+ }
1898
+ return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
1899
+ }
1809
1900
  function buildEvaluator(expr, scope) {
1810
1901
  const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1811
1902
  const cache = getScopeCache(scopeExpressionCache, scope);
@@ -1818,11 +1909,17 @@ function buildEvaluator(expr, scope) {
1818
1909
  }
1819
1910
  if (!_allowUnsafeEval) {
1820
1911
  dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
1821
- reportDiagnostic("expression-unsupported", cleaned, "Unsupported expression in CSP-safe mode");
1912
+ reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
1822
1913
  const blocked = () => void 0;
1823
1914
  cache.set(cleaned, blocked);
1824
1915
  return blocked;
1825
1916
  }
1917
+ const blockedMethod = findBlockedMethod(cleaned);
1918
+ if (blockedMethod) {
1919
+ const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
1920
+ reportDiagnostic("expression-unsupported", cleaned, msg);
1921
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1922
+ }
1826
1923
  try {
1827
1924
  const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
1828
1925
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1838,7 +1935,7 @@ function buildEvaluator(expr, scope) {
1838
1935
  cache.set(cleaned, unsafe);
1839
1936
  return unsafe;
1840
1937
  } catch {
1841
- reportDiagnostic("expression-unsupported", cleaned, "Expression failed to compile");
1938
+ 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.");
1842
1939
  const failed = () => void 0;
1843
1940
  cache.set(cleaned, failed);
1844
1941
  return failed;
@@ -1944,7 +2041,13 @@ function parseHandler(expr, scope) {
1944
2041
  return null;
1945
2042
  }
1946
2043
  function buildHandler(expr, scope) {
1947
- const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
2044
+ let cleaned = expr.trim();
2045
+ if (cleaned.startsWith("{")) {
2046
+ const seg = readBalancedSegment(cleaned, 0, "{", "}");
2047
+ if (seg && seg.end === cleaned.length - 1) {
2048
+ cleaned = seg.inner.trim();
2049
+ }
2050
+ }
1948
2051
  const cache = getScopeCache(scopeHandlerCache, scope);
1949
2052
  const cached = cache.get(cleaned);
1950
2053
  if (cached) return cached;
@@ -1956,7 +2059,7 @@ function buildHandler(expr, scope) {
1956
2059
  }
1957
2060
  if (!_allowUnsafeEval) {
1958
2061
  dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
1959
- reportDiagnostic("handler-unsupported", cleaned, "Unsupported handler in CSP-safe mode");
2062
+ reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
1960
2063
  const result = {
1961
2064
  handler: () => {
1962
2065
  },
@@ -1965,6 +2068,12 @@ function buildHandler(expr, scope) {
1965
2068
  cache.set(cleaned, result);
1966
2069
  return result;
1967
2070
  }
2071
+ const blockedMethod = findBlockedMethod(cleaned);
2072
+ if (blockedMethod) {
2073
+ const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
2074
+ reportDiagnostic("handler-unsupported", cleaned, msg);
2075
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
2076
+ }
1968
2077
  try {
1969
2078
  const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
1970
2079
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1992,7 +2101,7 @@ function buildHandler(expr, scope) {
1992
2101
  cache.set(cleaned, result);
1993
2102
  return result;
1994
2103
  } catch {
1995
- reportDiagnostic("handler-unsupported", cleaned, "Handler failed to compile");
2104
+ 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.");
1996
2105
  const result = {
1997
2106
  handler: () => {
1998
2107
  },
@@ -2002,16 +2111,22 @@ function buildHandler(expr, scope) {
2002
2111
  return result;
2003
2112
  }
2004
2113
  }
2114
+ var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2005
2115
  function parseState(raw) {
2116
+ let parsed;
2006
2117
  try {
2007
- return JSON.parse(raw);
2118
+ parsed = JSON.parse(raw);
2008
2119
  } catch {
2009
2120
  try {
2010
- return JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
2121
+ parsed = JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
2011
2122
  } catch {
2012
2123
  return {};
2013
2124
  }
2014
2125
  }
2126
+ for (const key of FORBIDDEN_STATE_KEYS) {
2127
+ if (key in parsed) delete parsed[key];
2128
+ }
2129
+ return parsed;
2015
2130
  }
2016
2131
  function initScope(stateEl) {
2017
2132
  const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
@@ -2681,6 +2796,7 @@ function setUnsafeEvalMode(mode) {
2681
2796
  _unsafeEvalMode = mode;
2682
2797
  if (mode === "locked-off") _allowUnsafeEval = false;
2683
2798
  if (mode === "locked-on") _allowUnsafeEval = true;
2799
+ if (mode === "mutable") _allowUnsafeEval = true;
2684
2800
  scopeExpressionCache = /* @__PURE__ */ new WeakMap();
2685
2801
  scopeHandlerCache = /* @__PURE__ */ new WeakMap();
2686
2802
  }