@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
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { internalEffect, reconcileList, batch } from './chunk-VKKPX4YU.js';
1
+ import { internalEffect, reconcileList, batch } from './chunk-5H52PKGF.js';
2
2
  import { createValueSignal, createComputed } from './chunk-KX5WRZH7.js';
3
3
 
4
4
  // src/dom/reconcile.ts
@@ -479,6 +479,7 @@ if (buildUnsafeEvalMode) {
479
479
  _unsafeEvalMode = buildUnsafeEvalMode;
480
480
  if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
481
481
  if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
482
+ if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
482
483
  }
483
484
  var runtimeConfig = readRuntimeConfig();
484
485
  var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
@@ -556,7 +557,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
556
557
  var RE_NUMBER = /^-?\d+(\.\d+)?$/;
557
558
  var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
558
559
  var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
559
- var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
560
560
  var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
561
561
  var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
562
562
  var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
@@ -567,7 +567,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
567
567
  var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
568
568
  var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
569
569
  var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
570
- var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
571
570
  var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
572
571
  var RE_STRIP_BRACES = /^\{|\}$/g;
573
572
  var RE_DIGIT_ONLY = /^\d+$/;
@@ -582,6 +581,7 @@ var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
582
581
  var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
583
582
  var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
584
583
  var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
584
+ var RE_EVENT_REF = /\bevent\b|\$event\b/;
585
585
  var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
586
586
  var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
587
587
  var EXPRESSION_CACHE_MAX = 2048;
@@ -596,7 +596,40 @@ function cacheExpression(key, factory) {
596
596
  var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
597
597
  var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
598
598
  var compiledTemplateCache = /* @__PURE__ */ new Map();
599
- var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "__proto__", "prototype"]);
599
+ var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
600
+ "constructor",
601
+ "__proto__",
602
+ "prototype",
603
+ "__defineGetter__",
604
+ "__defineSetter__",
605
+ "__lookupGetter__",
606
+ "__lookupSetter__",
607
+ "eval"
608
+ ]);
609
+ var BLOCKED_METHOD_REGEXES = (() => {
610
+ const result = [];
611
+ for (const name of UNSAFE_METHOD_NAMES) {
612
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
613
+ result.push({
614
+ name,
615
+ // Match as property access (.name) or bare identifier at start
616
+ dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
617
+ // Match bracket access with single quotes, double quotes, or backticks
618
+ bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
619
+ });
620
+ }
621
+ return result;
622
+ })();
623
+ function findBlockedMethod(expr) {
624
+ let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
625
+ cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
626
+ cleaned = cleaned.replace(/\s*\.\s*/g, ".");
627
+ for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
628
+ if (dotRe.test(cleaned)) return name;
629
+ if (bracketRe.test(cleaned)) return name;
630
+ }
631
+ return null;
632
+ }
600
633
  var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
601
634
  function toTextValue(value) {
602
635
  if (value == null) return "";
@@ -871,6 +904,7 @@ function consumeStatement(raw) {
871
904
  function parseIfHandler(expr, scope) {
872
905
  const input = expr.trim();
873
906
  if (!RE_IF_PREFIX.test(input)) return null;
907
+ if (RE_EVENT_REF.test(input)) return null;
874
908
  let idx = 2;
875
909
  while (idx < input.length && /\s/.test(input[idx])) idx++;
876
910
  if (input[idx] !== "(") return null;
@@ -1272,6 +1306,68 @@ function cloneAttributeTemplates(el, item) {
1272
1306
  }
1273
1307
  }
1274
1308
  }
1309
+ function parseChainedAccess(expr, scope) {
1310
+ let pos = 0;
1311
+ const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
1312
+ if (!identMatch) return null;
1313
+ const rootName = identMatch[0];
1314
+ pos = rootName.length;
1315
+ if (pos >= expr.length) return null;
1316
+ if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
1317
+ const steps = [];
1318
+ while (pos < expr.length) {
1319
+ let optional = false;
1320
+ if (expr[pos] === "?" && expr[pos + 1] === ".") {
1321
+ optional = true;
1322
+ pos += 2;
1323
+ } else if (expr[pos] === ".") {
1324
+ pos += 1;
1325
+ } else {
1326
+ return null;
1327
+ }
1328
+ const nameMatch = expr.slice(pos).match(/^\w+/);
1329
+ if (!nameMatch) return null;
1330
+ const name = nameMatch[0];
1331
+ pos += name.length;
1332
+ if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
1333
+ if (pos < expr.length && expr[pos] === "(") {
1334
+ const balanced = readBalancedSegment(expr, pos, "(", ")");
1335
+ if (!balanced) return null;
1336
+ const argsRaw = balanced.inner.trim();
1337
+ const argFns = [];
1338
+ for (const arg of splitCallArgs(argsRaw)) {
1339
+ const parsed = parseExpression(arg, scope);
1340
+ if (!parsed) return null;
1341
+ argFns.push(parsed);
1342
+ }
1343
+ steps.push({ type: "call", name, optional, argFns });
1344
+ pos = balanced.end + 1;
1345
+ } else {
1346
+ steps.push({ type: "prop", name, optional });
1347
+ }
1348
+ }
1349
+ if (pos !== expr.length) return null;
1350
+ if (steps.length === 0) return null;
1351
+ const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
1352
+ return () => {
1353
+ let val = rootExpr();
1354
+ for (const step of steps) {
1355
+ if (val == null) {
1356
+ if (step.optional) return void 0;
1357
+ return void 0;
1358
+ }
1359
+ if (step.type === "prop") {
1360
+ val = val[step.name];
1361
+ } else {
1362
+ const method = val[step.name];
1363
+ if (typeof method !== "function") return void 0;
1364
+ const args = step.argFns.map((fn) => fn());
1365
+ val = method.apply(val, args);
1366
+ }
1367
+ }
1368
+ return val;
1369
+ };
1370
+ }
1275
1371
  function parseExpression(expr, scope) {
1276
1372
  const cachedFactory = expressionCache.get(expr);
1277
1373
  if (cachedFactory) return cachedFactory(scope);
@@ -1322,24 +1418,9 @@ function parseExpressionUncached(expr, scope) {
1322
1418
  if (RE_IDENTIFIER.test(expr)) {
1323
1419
  return () => scope.getters[expr]?.();
1324
1420
  }
1325
- const dotMatch = expr.match(RE_DOT_ACCESS);
1326
- if (dotMatch) {
1327
- const p1 = dotMatch[1], p2 = dotMatch[2];
1328
- return () => {
1329
- const obj = scope.getters[p1]?.();
1330
- return obj?.[p2];
1331
- };
1332
- }
1333
- const deepDotMatch = expr.match(RE_DEEP_DOT);
1334
- if (deepDotMatch) {
1335
- const p1 = deepDotMatch[1], p2 = deepDotMatch[2], p3 = deepDotMatch[3], p4 = deepDotMatch[4];
1336
- return () => {
1337
- let val = scope.getters[p1]?.();
1338
- val = val?.[p2];
1339
- val = val?.[p3];
1340
- if (p4) val = val?.[p4];
1341
- return val;
1342
- };
1421
+ {
1422
+ const chainResult = parseChainedAccess(expr, scope);
1423
+ if (chainResult) return chainResult;
1343
1424
  }
1344
1425
  const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
1345
1426
  if (groupedCallMatch) {
@@ -1363,28 +1444,6 @@ function parseExpressionUncached(expr, scope) {
1363
1444
  return method.apply(base, args);
1364
1445
  };
1365
1446
  }
1366
- const callMatch = expr.match(RE_METHOD_CALL);
1367
- if (callMatch) {
1368
- const baseName = callMatch[1];
1369
- const methodName = callMatch[2];
1370
- const argsRaw = callMatch[3].trim();
1371
- if (UNSAFE_METHOD_NAMES.has(methodName)) return () => void 0;
1372
- const baseExpr = baseName === "Math" ? (() => Math) : parseExpression(baseName, scope);
1373
- if (!baseExpr) return null;
1374
- const argFns = [];
1375
- for (const arg of splitCallArgs(argsRaw)) {
1376
- const parsed = parseExpression(arg, scope);
1377
- if (!parsed) return null;
1378
- argFns.push(parsed);
1379
- }
1380
- return () => {
1381
- const base = baseExpr();
1382
- const method = base?.[methodName];
1383
- if (typeof method !== "function") return void 0;
1384
- const args = argFns.map((fn) => fn());
1385
- return method.apply(base, args);
1386
- };
1387
- }
1388
1447
  if (expr.startsWith("!")) {
1389
1448
  const inner = parseExpression(expr.slice(1).trim(), scope);
1390
1449
  if (inner) return () => !inner();
@@ -1403,6 +1462,29 @@ function parseExpressionUncached(expr, scope) {
1403
1462
  return () => objExpr()?.[key];
1404
1463
  }
1405
1464
  }
1465
+ if (expr.startsWith("[")) {
1466
+ const balanced = readBalancedSegment(expr, 0, "[", "]");
1467
+ if (balanced && balanced.end === expr.length - 1) {
1468
+ const inner = balanced.inner.trim();
1469
+ if (inner === "") {
1470
+ return () => [];
1471
+ }
1472
+ const elements = splitCallArgs(inner);
1473
+ const elementFns = [];
1474
+ let allParsed = true;
1475
+ for (const el of elements) {
1476
+ const parsed = parseExpression(el.trim(), scope);
1477
+ if (!parsed) {
1478
+ allParsed = false;
1479
+ break;
1480
+ }
1481
+ elementFns.push(parsed);
1482
+ }
1483
+ if (allParsed) {
1484
+ return () => elementFns.map((fn) => fn());
1485
+ }
1486
+ }
1487
+ }
1406
1488
  const ternaryMatch = expr.match(RE_TERNARY);
1407
1489
  if (ternaryMatch) {
1408
1490
  const cond = parseExpression(ternaryMatch[1].trim(), scope);
@@ -1532,6 +1614,15 @@ function getScopeCache(cache, scope) {
1532
1614
  }
1533
1615
  return scoped;
1534
1616
  }
1617
+ function cspExpressionHint(expr) {
1618
+ if (expr.includes("...")) {
1619
+ return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
1620
+ }
1621
+ if (expr.includes("=>")) {
1622
+ return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
1623
+ }
1624
+ return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
1625
+ }
1535
1626
  function buildEvaluator(expr, scope) {
1536
1627
  const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1537
1628
  const cache = getScopeCache(scopeExpressionCache, scope);
@@ -1544,11 +1635,17 @@ function buildEvaluator(expr, scope) {
1544
1635
  }
1545
1636
  if (!_allowUnsafeEval) {
1546
1637
  dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
1547
- reportDiagnostic("expression-unsupported", cleaned, "Unsupported expression in CSP-safe mode");
1638
+ reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
1548
1639
  const blocked = () => void 0;
1549
1640
  cache.set(cleaned, blocked);
1550
1641
  return blocked;
1551
1642
  }
1643
+ const blockedMethod = findBlockedMethod(cleaned);
1644
+ if (blockedMethod) {
1645
+ const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
1646
+ reportDiagnostic("expression-unsupported", cleaned, msg);
1647
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1648
+ }
1552
1649
  try {
1553
1650
  const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
1554
1651
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1564,7 +1661,7 @@ function buildEvaluator(expr, scope) {
1564
1661
  cache.set(cleaned, unsafe);
1565
1662
  return unsafe;
1566
1663
  } catch {
1567
- reportDiagnostic("expression-unsupported", cleaned, "Expression failed to compile");
1664
+ 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.");
1568
1665
  const failed = () => void 0;
1569
1666
  cache.set(cleaned, failed);
1570
1667
  return failed;
@@ -1670,7 +1767,13 @@ function parseHandler(expr, scope) {
1670
1767
  return null;
1671
1768
  }
1672
1769
  function buildHandler(expr, scope) {
1673
- const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1770
+ let cleaned = expr.trim();
1771
+ if (cleaned.startsWith("{")) {
1772
+ const seg = readBalancedSegment(cleaned, 0, "{", "}");
1773
+ if (seg && seg.end === cleaned.length - 1) {
1774
+ cleaned = seg.inner.trim();
1775
+ }
1776
+ }
1674
1777
  const cache = getScopeCache(scopeHandlerCache, scope);
1675
1778
  const cached = cache.get(cleaned);
1676
1779
  if (cached) return cached;
@@ -1682,7 +1785,7 @@ function buildHandler(expr, scope) {
1682
1785
  }
1683
1786
  if (!_allowUnsafeEval) {
1684
1787
  dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
1685
- reportDiagnostic("handler-unsupported", cleaned, "Unsupported handler in CSP-safe mode");
1788
+ reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
1686
1789
  const result = {
1687
1790
  handler: () => {
1688
1791
  },
@@ -1691,6 +1794,12 @@ function buildHandler(expr, scope) {
1691
1794
  cache.set(cleaned, result);
1692
1795
  return result;
1693
1796
  }
1797
+ const blockedMethod = findBlockedMethod(cleaned);
1798
+ if (blockedMethod) {
1799
+ const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
1800
+ reportDiagnostic("handler-unsupported", cleaned, msg);
1801
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1802
+ }
1694
1803
  try {
1695
1804
  const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
1696
1805
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1718,7 +1827,7 @@ function buildHandler(expr, scope) {
1718
1827
  cache.set(cleaned, result);
1719
1828
  return result;
1720
1829
  } catch {
1721
- reportDiagnostic("handler-unsupported", cleaned, "Handler failed to compile");
1830
+ 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.");
1722
1831
  const result = {
1723
1832
  handler: () => {
1724
1833
  },
@@ -1728,16 +1837,22 @@ function buildHandler(expr, scope) {
1728
1837
  return result;
1729
1838
  }
1730
1839
  }
1840
+ var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1731
1841
  function parseState(raw) {
1842
+ let parsed;
1732
1843
  try {
1733
- return JSON.parse(raw);
1844
+ parsed = JSON.parse(raw);
1734
1845
  } catch {
1735
1846
  try {
1736
- return JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
1847
+ parsed = JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
1737
1848
  } catch {
1738
1849
  return {};
1739
1850
  }
1740
1851
  }
1852
+ for (const key of FORBIDDEN_STATE_KEYS) {
1853
+ if (key in parsed) delete parsed[key];
1854
+ }
1855
+ return parsed;
1741
1856
  }
1742
1857
  function initScope(stateEl) {
1743
1858
  const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
@@ -2407,6 +2522,7 @@ function setUnsafeEvalMode(mode) {
2407
2522
  _unsafeEvalMode = mode;
2408
2523
  if (mode === "locked-off") _allowUnsafeEval = false;
2409
2524
  if (mode === "locked-on") _allowUnsafeEval = true;
2525
+ if (mode === "mutable") _allowUnsafeEval = true;
2410
2526
  scopeExpressionCache = /* @__PURE__ */ new WeakMap();
2411
2527
  scopeHandlerCache = /* @__PURE__ */ new WeakMap();
2412
2528
  }