@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
@@ -755,6 +755,7 @@ if (buildUnsafeEvalMode) {
755
755
  _unsafeEvalMode = buildUnsafeEvalMode;
756
756
  if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
757
757
  if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
758
+ if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
758
759
  }
759
760
  var runtimeConfig = readRuntimeConfig();
760
761
  var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
@@ -832,7 +833,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
832
833
  var RE_NUMBER = /^-?\d+(\.\d+)?$/;
833
834
  var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
834
835
  var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
835
- var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
836
836
  var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
837
837
  var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
838
838
  var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
@@ -843,7 +843,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
843
843
  var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
844
844
  var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
845
845
  var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
846
- var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
847
846
  var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
848
847
  var RE_STRIP_BRACES = /^\{|\}$/g;
849
848
  var RE_DIGIT_ONLY = /^\d+$/;
@@ -858,6 +857,7 @@ var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
858
857
  var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
859
858
  var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
860
859
  var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
860
+ var RE_EVENT_REF = /\bevent\b|\$event\b/;
861
861
  var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
862
862
  var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
863
863
  var EXPRESSION_CACHE_MAX = 2048;
@@ -872,7 +872,40 @@ function cacheExpression(key, factory) {
872
872
  var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
873
873
  var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
874
874
  var compiledTemplateCache = /* @__PURE__ */ new Map();
875
- var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "__proto__", "prototype"]);
875
+ var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
876
+ "constructor",
877
+ "__proto__",
878
+ "prototype",
879
+ "__defineGetter__",
880
+ "__defineSetter__",
881
+ "__lookupGetter__",
882
+ "__lookupSetter__",
883
+ "eval"
884
+ ]);
885
+ var BLOCKED_METHOD_REGEXES = (() => {
886
+ const result = [];
887
+ for (const name of UNSAFE_METHOD_NAMES) {
888
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
889
+ result.push({
890
+ name,
891
+ // Match as property access (.name) or bare identifier at start
892
+ dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
893
+ // Match bracket access with single quotes, double quotes, or backticks
894
+ bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
895
+ });
896
+ }
897
+ return result;
898
+ })();
899
+ function findBlockedMethod(expr) {
900
+ let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
901
+ cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
902
+ cleaned = cleaned.replace(/\s*\.\s*/g, ".");
903
+ for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
904
+ if (dotRe.test(cleaned)) return name;
905
+ if (bracketRe.test(cleaned)) return name;
906
+ }
907
+ return null;
908
+ }
876
909
  var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
877
910
  function toTextValue(value2) {
878
911
  if (value2 == null) return "";
@@ -1147,6 +1180,7 @@ function consumeStatement(raw) {
1147
1180
  function parseIfHandler(expr, scope) {
1148
1181
  const input = expr.trim();
1149
1182
  if (!RE_IF_PREFIX.test(input)) return null;
1183
+ if (RE_EVENT_REF.test(input)) return null;
1150
1184
  let idx = 2;
1151
1185
  while (idx < input.length && /\s/.test(input[idx])) idx++;
1152
1186
  if (input[idx] !== "(") return null;
@@ -1548,6 +1582,68 @@ function cloneAttributeTemplates(el, item) {
1548
1582
  }
1549
1583
  }
1550
1584
  }
1585
+ function parseChainedAccess(expr, scope) {
1586
+ let pos = 0;
1587
+ const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
1588
+ if (!identMatch) return null;
1589
+ const rootName = identMatch[0];
1590
+ pos = rootName.length;
1591
+ if (pos >= expr.length) return null;
1592
+ if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
1593
+ const steps = [];
1594
+ while (pos < expr.length) {
1595
+ let optional = false;
1596
+ if (expr[pos] === "?" && expr[pos + 1] === ".") {
1597
+ optional = true;
1598
+ pos += 2;
1599
+ } else if (expr[pos] === ".") {
1600
+ pos += 1;
1601
+ } else {
1602
+ return null;
1603
+ }
1604
+ const nameMatch = expr.slice(pos).match(/^\w+/);
1605
+ if (!nameMatch) return null;
1606
+ const name = nameMatch[0];
1607
+ pos += name.length;
1608
+ if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
1609
+ if (pos < expr.length && expr[pos] === "(") {
1610
+ const balanced = readBalancedSegment(expr, pos, "(", ")");
1611
+ if (!balanced) return null;
1612
+ const argsRaw = balanced.inner.trim();
1613
+ const argFns = [];
1614
+ for (const arg of splitCallArgs(argsRaw)) {
1615
+ const parsed = parseExpression(arg, scope);
1616
+ if (!parsed) return null;
1617
+ argFns.push(parsed);
1618
+ }
1619
+ steps.push({ type: "call", name, optional, argFns });
1620
+ pos = balanced.end + 1;
1621
+ } else {
1622
+ steps.push({ type: "prop", name, optional });
1623
+ }
1624
+ }
1625
+ if (pos !== expr.length) return null;
1626
+ if (steps.length === 0) return null;
1627
+ const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
1628
+ return () => {
1629
+ let val = rootExpr();
1630
+ for (const step of steps) {
1631
+ if (val == null) {
1632
+ if (step.optional) return void 0;
1633
+ return void 0;
1634
+ }
1635
+ if (step.type === "prop") {
1636
+ val = val[step.name];
1637
+ } else {
1638
+ const method = val[step.name];
1639
+ if (typeof method !== "function") return void 0;
1640
+ const args = step.argFns.map((fn) => fn());
1641
+ val = method.apply(val, args);
1642
+ }
1643
+ }
1644
+ return val;
1645
+ };
1646
+ }
1551
1647
  function parseExpression(expr, scope) {
1552
1648
  const cachedFactory = expressionCache.get(expr);
1553
1649
  if (cachedFactory) return cachedFactory(scope);
@@ -1598,24 +1694,9 @@ function parseExpressionUncached(expr, scope) {
1598
1694
  if (RE_IDENTIFIER.test(expr)) {
1599
1695
  return () => scope.getters[expr]?.();
1600
1696
  }
1601
- const dotMatch = expr.match(RE_DOT_ACCESS);
1602
- if (dotMatch) {
1603
- const p1 = dotMatch[1], p2 = dotMatch[2];
1604
- return () => {
1605
- const obj = scope.getters[p1]?.();
1606
- return obj?.[p2];
1607
- };
1608
- }
1609
- const deepDotMatch = expr.match(RE_DEEP_DOT);
1610
- if (deepDotMatch) {
1611
- const p1 = deepDotMatch[1], p2 = deepDotMatch[2], p3 = deepDotMatch[3], p4 = deepDotMatch[4];
1612
- return () => {
1613
- let val = scope.getters[p1]?.();
1614
- val = val?.[p2];
1615
- val = val?.[p3];
1616
- if (p4) val = val?.[p4];
1617
- return val;
1618
- };
1697
+ {
1698
+ const chainResult = parseChainedAccess(expr, scope);
1699
+ if (chainResult) return chainResult;
1619
1700
  }
1620
1701
  const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
1621
1702
  if (groupedCallMatch) {
@@ -1639,28 +1720,6 @@ function parseExpressionUncached(expr, scope) {
1639
1720
  return method.apply(base, args);
1640
1721
  };
1641
1722
  }
1642
- const callMatch = expr.match(RE_METHOD_CALL);
1643
- if (callMatch) {
1644
- const baseName = callMatch[1];
1645
- const methodName = callMatch[2];
1646
- const argsRaw = callMatch[3].trim();
1647
- if (UNSAFE_METHOD_NAMES.has(methodName)) return () => void 0;
1648
- const baseExpr = baseName === "Math" ? (() => Math) : parseExpression(baseName, scope);
1649
- if (!baseExpr) return null;
1650
- const argFns = [];
1651
- for (const arg of splitCallArgs(argsRaw)) {
1652
- const parsed = parseExpression(arg, scope);
1653
- if (!parsed) return null;
1654
- argFns.push(parsed);
1655
- }
1656
- return () => {
1657
- const base = baseExpr();
1658
- const method = base?.[methodName];
1659
- if (typeof method !== "function") return void 0;
1660
- const args = argFns.map((fn) => fn());
1661
- return method.apply(base, args);
1662
- };
1663
- }
1664
1723
  if (expr.startsWith("!")) {
1665
1724
  const inner = parseExpression(expr.slice(1).trim(), scope);
1666
1725
  if (inner) return () => !inner();
@@ -1679,6 +1738,29 @@ function parseExpressionUncached(expr, scope) {
1679
1738
  return () => objExpr()?.[key];
1680
1739
  }
1681
1740
  }
1741
+ if (expr.startsWith("[")) {
1742
+ const balanced = readBalancedSegment(expr, 0, "[", "]");
1743
+ if (balanced && balanced.end === expr.length - 1) {
1744
+ const inner = balanced.inner.trim();
1745
+ if (inner === "") {
1746
+ return () => [];
1747
+ }
1748
+ const elements = splitCallArgs(inner);
1749
+ const elementFns = [];
1750
+ let allParsed = true;
1751
+ for (const el of elements) {
1752
+ const parsed = parseExpression(el.trim(), scope);
1753
+ if (!parsed) {
1754
+ allParsed = false;
1755
+ break;
1756
+ }
1757
+ elementFns.push(parsed);
1758
+ }
1759
+ if (allParsed) {
1760
+ return () => elementFns.map((fn) => fn());
1761
+ }
1762
+ }
1763
+ }
1682
1764
  const ternaryMatch = expr.match(RE_TERNARY);
1683
1765
  if (ternaryMatch) {
1684
1766
  const cond = parseExpression(ternaryMatch[1].trim(), scope);
@@ -1808,6 +1890,15 @@ function getScopeCache(cache, scope) {
1808
1890
  }
1809
1891
  return scoped;
1810
1892
  }
1893
+ function cspExpressionHint(expr) {
1894
+ if (expr.includes("...")) {
1895
+ return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
1896
+ }
1897
+ if (expr.includes("=>")) {
1898
+ return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
1899
+ }
1900
+ return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
1901
+ }
1811
1902
  function buildEvaluator(expr, scope) {
1812
1903
  const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1813
1904
  const cache = getScopeCache(scopeExpressionCache, scope);
@@ -1820,11 +1911,17 @@ function buildEvaluator(expr, scope) {
1820
1911
  }
1821
1912
  if (!_allowUnsafeEval) {
1822
1913
  dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
1823
- reportDiagnostic("expression-unsupported", cleaned, "Unsupported expression in CSP-safe mode");
1914
+ reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
1824
1915
  const blocked = () => void 0;
1825
1916
  cache.set(cleaned, blocked);
1826
1917
  return blocked;
1827
1918
  }
1919
+ const blockedMethod = findBlockedMethod(cleaned);
1920
+ if (blockedMethod) {
1921
+ const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
1922
+ reportDiagnostic("expression-unsupported", cleaned, msg);
1923
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1924
+ }
1828
1925
  try {
1829
1926
  const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
1830
1927
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1840,7 +1937,7 @@ function buildEvaluator(expr, scope) {
1840
1937
  cache.set(cleaned, unsafe);
1841
1938
  return unsafe;
1842
1939
  } catch {
1843
- reportDiagnostic("expression-unsupported", cleaned, "Expression failed to compile");
1940
+ 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.");
1844
1941
  const failed = () => void 0;
1845
1942
  cache.set(cleaned, failed);
1846
1943
  return failed;
@@ -1946,7 +2043,13 @@ function parseHandler(expr, scope) {
1946
2043
  return null;
1947
2044
  }
1948
2045
  function buildHandler(expr, scope) {
1949
- const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
2046
+ let cleaned = expr.trim();
2047
+ if (cleaned.startsWith("{")) {
2048
+ const seg = readBalancedSegment(cleaned, 0, "{", "}");
2049
+ if (seg && seg.end === cleaned.length - 1) {
2050
+ cleaned = seg.inner.trim();
2051
+ }
2052
+ }
1950
2053
  const cache = getScopeCache(scopeHandlerCache, scope);
1951
2054
  const cached = cache.get(cleaned);
1952
2055
  if (cached) return cached;
@@ -1958,7 +2061,7 @@ function buildHandler(expr, scope) {
1958
2061
  }
1959
2062
  if (!_allowUnsafeEval) {
1960
2063
  dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
1961
- reportDiagnostic("handler-unsupported", cleaned, "Unsupported handler in CSP-safe mode");
2064
+ reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
1962
2065
  const result = {
1963
2066
  handler: () => {
1964
2067
  },
@@ -1967,6 +2070,12 @@ function buildHandler(expr, scope) {
1967
2070
  cache.set(cleaned, result);
1968
2071
  return result;
1969
2072
  }
2073
+ const blockedMethod = findBlockedMethod(cleaned);
2074
+ if (blockedMethod) {
2075
+ const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
2076
+ reportDiagnostic("handler-unsupported", cleaned, msg);
2077
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
2078
+ }
1970
2079
  try {
1971
2080
  const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
1972
2081
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1994,7 +2103,7 @@ function buildHandler(expr, scope) {
1994
2103
  cache.set(cleaned, result);
1995
2104
  return result;
1996
2105
  } catch {
1997
- reportDiagnostic("handler-unsupported", cleaned, "Handler failed to compile");
2106
+ 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.");
1998
2107
  const result = {
1999
2108
  handler: () => {
2000
2109
  },
@@ -2004,16 +2113,22 @@ function buildHandler(expr, scope) {
2004
2113
  return result;
2005
2114
  }
2006
2115
  }
2116
+ var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2007
2117
  function parseState(raw) {
2118
+ let parsed;
2008
2119
  try {
2009
- return JSON.parse(raw);
2120
+ parsed = JSON.parse(raw);
2010
2121
  } catch {
2011
2122
  try {
2012
- return JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
2123
+ parsed = JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
2013
2124
  } catch {
2014
2125
  return {};
2015
2126
  }
2016
2127
  }
2128
+ for (const key of FORBIDDEN_STATE_KEYS) {
2129
+ if (key in parsed) delete parsed[key];
2130
+ }
2131
+ return parsed;
2017
2132
  }
2018
2133
  function initScope(stateEl) {
2019
2134
  const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
@@ -2683,6 +2798,7 @@ function setUnsafeEvalMode(mode) {
2683
2798
  _unsafeEvalMode = mode;
2684
2799
  if (mode === "locked-off") _allowUnsafeEval = false;
2685
2800
  if (mode === "locked-on") _allowUnsafeEval = true;
2801
+ if (mode === "mutable") _allowUnsafeEval = true;
2686
2802
  scopeExpressionCache = /* @__PURE__ */ new WeakMap();
2687
2803
  scopeHandlerCache = /* @__PURE__ */ new WeakMap();
2688
2804
  }