bonsai-js 0.4.0 → 0.5.0

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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { a as attachLocation, c as offsetToPosition, i as ExpressionError, l as suggest, n as BonsaiSecurityError, o as formatBonsaiError, r as BonsaiTypeError, s as formatError, t as BonsaiReferenceError } from "./errors-BONB6P_5.mjs";
2
- import { i as tokenize, n as ExecutionContext, r as SecurityPolicy } from "./execution-context-B-5hQ92X.mjs";
1
+ import { a as attachLocation, c as offsetToPosition, i as ExpressionError, l as suggest, n as BonsaiSecurityError, o as formatBonsaiError, r as BonsaiTypeError, s as formatError, t as BonsaiReferenceError } from "./errors-CCZDd1gK.mjs";
2
+ import { i as isMethodAllowedOn, n as ExecutionContext, o as tokenize, r as SecurityPolicy } from "./execution-context-b6FBfEdv.mjs";
3
3
  //#region src/parser.ts
4
4
  const PIPE_PRECEDENCE = 5;
5
5
  const PRECEDENCE = {
@@ -23,6 +23,7 @@ const PRECEDENCE = {
23
23
  "not": void 0
24
24
  };
25
25
  const MAX_PARSE_DEPTH = 32;
26
+ const MAX_GRAMMAR_DEPTH = 1e3;
26
27
  function parse(source, _depth = 0) {
27
28
  if (_depth > MAX_PARSE_DEPTH) throw new ExpressionError("Maximum template nesting depth exceeded", {
28
29
  source,
@@ -31,6 +32,7 @@ function parse(source, _depth = 0) {
31
32
  });
32
33
  const tokens = tokenize(source);
33
34
  let pos = 0;
35
+ let grammarDepth = 0;
34
36
  function current() {
35
37
  return tokens[pos];
36
38
  }
@@ -46,7 +48,26 @@ function parse(source, _depth = 0) {
46
48
  });
47
49
  return advance();
48
50
  }
51
+ function enterNesting() {
52
+ if (++grammarDepth > MAX_GRAMMAR_DEPTH) {
53
+ grammarDepth--;
54
+ const tok = current();
55
+ throw new ExpressionError("Maximum expression nesting depth exceeded", {
56
+ source,
57
+ start: tok.start,
58
+ end: tok.end
59
+ });
60
+ }
61
+ }
49
62
  function parseExpression(minPrec = 0) {
63
+ enterNesting();
64
+ try {
65
+ return parseExpressionInner(minPrec);
66
+ } finally {
67
+ grammarDepth--;
68
+ }
69
+ }
70
+ function parseExpressionInner(minPrec = 0) {
50
71
  let left = parseUnary();
51
72
  if (current().type === "Punctuation" && current().value === "?" && minPrec === 0) {
52
73
  advance();
@@ -151,6 +172,14 @@ function parse(source, _depth = 0) {
151
172
  return node;
152
173
  }
153
174
  function parseUnary() {
175
+ enterNesting();
176
+ try {
177
+ return parseUnaryInner();
178
+ } finally {
179
+ grammarDepth--;
180
+ }
181
+ }
182
+ function parseUnaryInner() {
154
183
  const tok = current();
155
184
  if (tok.type === "Operator" && (tok.value === "!" || tok.value === "-" || tok.value === "+")) {
156
185
  advance();
@@ -610,8 +639,18 @@ function parse(source, _depth = 0) {
610
639
  let depth = 1;
611
640
  const exprStart = i;
612
641
  while (i < raw.length && depth > 0) {
613
- if (raw[i] === "{") depth++;
614
- else if (raw[i] === "}") depth--;
642
+ const c = raw[i];
643
+ if (c === "\"" || c === "'" || c === "`") {
644
+ i++;
645
+ while (i < raw.length && raw[i] !== c) {
646
+ if (raw[i] === "\\") i++;
647
+ i++;
648
+ }
649
+ i++;
650
+ continue;
651
+ }
652
+ if (c === "{") depth++;
653
+ else if (c === "}") depth--;
615
654
  if (depth > 0) i++;
616
655
  }
617
656
  const exprSource = raw.slice(exprStart, i);
@@ -791,50 +830,6 @@ function makeConstant(value, start, end) {
791
830
  }
792
831
  //#endregion
793
832
  //#region src/eval-ops.ts
794
- /** Receiver type check for safe methods. */
795
- function isAllowedReceiver(obj, methodName) {
796
- const t = typeof obj;
797
- switch (methodName) {
798
- case "startsWith":
799
- case "endsWith":
800
- case "substring":
801
- case "charAt":
802
- case "charCodeAt":
803
- case "repeat":
804
- case "trim":
805
- case "trimStart":
806
- case "trimEnd":
807
- case "toLowerCase":
808
- case "toUpperCase":
809
- case "replace":
810
- case "replaceAll":
811
- case "padStart":
812
- case "padEnd":
813
- case "split": return t === "string";
814
- case "includes":
815
- case "indexOf":
816
- case "lastIndexOf":
817
- case "slice":
818
- case "at":
819
- case "concat": return t === "string" || Array.isArray(obj);
820
- case "filter":
821
- case "map":
822
- case "find":
823
- case "findIndex":
824
- case "some":
825
- case "every":
826
- case "flatMap": return Array.isArray(obj);
827
- case "join":
828
- case "flat":
829
- case "toReversed":
830
- case "toSorted":
831
- case "toSpliced":
832
- case "with": return Array.isArray(obj);
833
- case "toFixed": return t === "number";
834
- case "toString": return t === "string" || t === "number";
835
- default: return false;
836
- }
837
- }
838
833
  function toPropertyKey(value) {
839
834
  return String(value);
840
835
  }
@@ -874,7 +869,7 @@ function applyUnaryOp(operator, operand) {
874
869
  function validateMethodCall(obj, methodName, guard) {
875
870
  guard.checkNameAccess(methodName, "method");
876
871
  if (obj == null) throw new BonsaiTypeError(methodName, "a non-null value", obj);
877
- if (!isAllowedReceiver(obj, methodName)) throw new BonsaiSecurityError("METHOD_NOT_ALLOWED", `Method "${methodName}" is not allowed on ${typeof obj}`);
872
+ if (!isMethodAllowedOn(obj, methodName)) throw new BonsaiSecurityError("METHOD_NOT_ALLOWED", `Method "${methodName}" is not allowed on ${typeof obj}`);
878
873
  const method = obj[methodName];
879
874
  if (typeof method !== "function") throw new BonsaiTypeError(methodName, "a method", method);
880
875
  return method;
@@ -921,7 +916,17 @@ function expandSpreadValue(value, maxLength) {
921
916
  }
922
917
  const REPLACE_METHODS = new Set(["replace", "replaceAll"]);
923
918
  const MAX_REPEAT_COUNT = 1e5;
924
- function validateMethodArgs(methodName, args) {
919
+ const HIGHER_ORDER_METHODS = new Set([
920
+ "map",
921
+ "filter",
922
+ "find",
923
+ "findIndex",
924
+ "some",
925
+ "every",
926
+ "flatMap"
927
+ ]);
928
+ const LAMBDA_CALLBACK_EXPECTED = "a lambda or function callback (e.g. .field or . > value)";
929
+ function validateMethodArgs(receiver, methodName, args, guard) {
925
930
  if (REPLACE_METHODS.has(methodName)) for (const arg of args) {
926
931
  if (typeof arg === "function") throw new BonsaiTypeError(methodName, "string arguments (callbacks are not allowed)", arg);
927
932
  if (arg instanceof RegExp) throw new BonsaiTypeError(methodName, "string arguments (RegExp is not allowed)", arg);
@@ -930,7 +935,32 @@ function validateMethodArgs(methodName, args) {
930
935
  if (methodName === "repeat") {
931
936
  const count = Number(args[0]);
932
937
  if (!Number.isFinite(count) || count < 0 || count > MAX_REPEAT_COUNT) throw new BonsaiTypeError("repeat", `a count between 0 and ${MAX_REPEAT_COUNT}`, args[0]);
938
+ if (typeof receiver === "string") guard.checkStringLength(receiver.length * count);
933
939
  }
940
+ if (methodName === "padStart" || methodName === "padEnd") guard.checkStringLength(Number(args[0]));
941
+ if (HIGHER_ORDER_METHODS.has(methodName) && typeof args[0] !== "function") throw new BonsaiTypeError(methodName, LAMBDA_CALLBACK_EXPECTED, args[0]);
942
+ }
943
+ /**
944
+ * Enforce the array-size limit on a value produced by a method call. Array
945
+ * literals and spread are checked at construction; array-returning methods
946
+ * (split, map, flat, concat, toSorted, ...) are checked here so maxArrayLength
947
+ * is a real ceiling on every array that flows through evaluation.
948
+ */
949
+ function checkResultArrayLength(result, guard) {
950
+ if (Array.isArray(result)) guard.checkArrayLength(result.length);
951
+ }
952
+ /**
953
+ * Enforce the string-size limit on a value produced by a method call. The
954
+ * argument-time checks on padStart/padEnd/repeat stop the worst single-call
955
+ * amplifiers before allocation, but string-returning methods such as join,
956
+ * concat, slice, and toUpperCase can still produce a string past the ceiling
957
+ * (notably `arr.join(sep)`, whose output is array length times separator length
958
+ * in a single native call). Checking the produced length here makes
959
+ * maxStringLength a real ceiling on every string that flows through evaluation,
960
+ * mirroring checkResultArrayLength.
961
+ */
962
+ function checkResultStringLength(result, guard) {
963
+ if (typeof result === "string") guard.checkStringLength(result.length);
934
964
  }
935
965
  function accessMember(object, propertyNode, computed, computedValue, guard) {
936
966
  const key = computed ? toPropertyKey(computedValue) : getIdentifierName(propertyNode, "Expected identifier property");
@@ -1061,8 +1091,11 @@ function evalCallExpression(node, env) {
1061
1091
  const method = validateMethodCall(obj, methodName, g);
1062
1092
  const args = [];
1063
1093
  for (const arg of node.args) pushCallArgument(args, arg, env);
1064
- validateMethodArgs(methodName, args);
1065
- return rejectPromise(method.call(obj, ...args), "method", methodName);
1094
+ validateMethodArgs(obj, methodName, args, g);
1095
+ const result = rejectPromise(method.call(obj, ...args), "method", methodName);
1096
+ checkResultArrayLength(result, g);
1097
+ checkResultStringLength(result, g);
1098
+ return result;
1066
1099
  } catch (e) {
1067
1100
  if (s) attachLocation(e, s, node.start, node.end);
1068
1101
  throw e;
@@ -1135,8 +1168,11 @@ function evalLambdaBody(node, item, env) {
1135
1168
  const args = [];
1136
1169
  for (const arg of node.args) if (arg.type === "SpreadElement") args.push(...expandSpreadValue(evalNode(arg.argument, env), g.policy.maxArrayLength));
1137
1170
  else args.push(evalArg(arg, env));
1138
- validateMethodArgs(methodName, args);
1139
- return rejectPromise(method.call(obj, ...args), "method", methodName);
1171
+ validateMethodArgs(obj, methodName, args, g);
1172
+ const result = rejectPromise(method.call(obj, ...args), "method", methodName);
1173
+ checkResultArrayLength(result, g);
1174
+ checkResultStringLength(result, g);
1175
+ return result;
1140
1176
  }
1141
1177
  ownDepth = false;
1142
1178
  g.exitDepth();
@@ -1302,18 +1338,21 @@ async function evalCallExpressionAsync(node, env) {
1302
1338
  const method = validateMethodCall(obj, methodName, g);
1303
1339
  const args = [];
1304
1340
  for (const arg of node.args) await pushCallArgumentAsync(args, arg, env);
1305
- validateMethodArgs(methodName, args);
1341
+ validateMethodArgs(obj, methodName, args, g);
1306
1342
  if (Array.isArray(obj) && args.length === 1 && typeof args[0] === "function") {
1307
1343
  const arr = obj;
1308
1344
  const predicate = args[0];
1309
1345
  const asyncResult = await evalAsyncArrayMethod(methodName, arr, predicate);
1310
1346
  if (asyncResult !== void 0) {
1311
1347
  g.checkTimeout();
1348
+ checkResultArrayLength(asyncResult.value, g);
1312
1349
  return asyncResult.value;
1313
1350
  }
1314
1351
  }
1315
1352
  const result = await method.call(obj, ...args);
1316
1353
  g.checkTimeout();
1354
+ checkResultArrayLength(result, g);
1355
+ checkResultStringLength(result, g);
1317
1356
  return result;
1318
1357
  } catch (e) {
1319
1358
  if (s) attachLocation(e, s, node.start, node.end);
@@ -1343,35 +1382,32 @@ async function pushCallArgumentAsync(args, node, env) {
1343
1382
  async function evalAsyncArrayMethod(methodName, arr, predicate) {
1344
1383
  switch (methodName) {
1345
1384
  case "filter": {
1346
- const results = arr.map(predicate);
1347
- const resolved = results.some((r) => r instanceof Promise) ? await Promise.all(results) : results;
1348
- return { value: arr.filter((_, i) => resolved[i]) };
1385
+ const out = [];
1386
+ for (const item of arr) if (await predicate(item)) out.push(item);
1387
+ return { value: out };
1349
1388
  }
1350
1389
  case "map": {
1351
- const results = arr.map(predicate);
1352
- return { value: results.some((r) => r instanceof Promise) ? await Promise.all(results) : results };
1353
- }
1354
- case "find": {
1355
- const results = arr.map(predicate);
1356
- const idx = (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).findIndex(Boolean);
1357
- return { value: idx >= 0 ? arr[idx] : void 0 };
1358
- }
1359
- case "some": {
1360
- const results = arr.map(predicate);
1361
- return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).some(Boolean) };
1362
- }
1363
- case "every": {
1364
- const results = arr.map(predicate);
1365
- return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).every(Boolean) };
1366
- }
1367
- case "findIndex": {
1368
- const results = arr.map(predicate);
1369
- return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).findIndex(Boolean) };
1390
+ const out = [];
1391
+ for (const item of arr) out.push(await predicate(item));
1392
+ return { value: out };
1370
1393
  }
1371
1394
  case "flatMap": {
1372
- const results = arr.map(predicate);
1373
- return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).flat() };
1395
+ const out = [];
1396
+ for (const item of arr) out.push(await predicate(item));
1397
+ return { value: out.flat() };
1374
1398
  }
1399
+ case "find":
1400
+ for (const item of arr) if (await predicate(item)) return { value: item };
1401
+ return { value: void 0 };
1402
+ case "findIndex":
1403
+ for (let i = 0; i < arr.length; i++) if (await predicate(arr[i])) return { value: i };
1404
+ return { value: -1 };
1405
+ case "some":
1406
+ for (const item of arr) if (await predicate(item)) return { value: true };
1407
+ return { value: false };
1408
+ case "every":
1409
+ for (const item of arr) if (!await predicate(item)) return { value: false };
1410
+ return { value: true };
1375
1411
  default: return;
1376
1412
  }
1377
1413
  }
@@ -1429,15 +1465,19 @@ async function evalLambdaBodyAsync(node, item, env) {
1429
1465
  const args = [];
1430
1466
  for (const arg of node.args) if (arg.type === "SpreadElement") args.push(...expandSpreadValue(await evalNodeAsync(arg.argument, env), g.policy.maxArrayLength));
1431
1467
  else args.push(await evalArgAsync(arg, env));
1432
- validateMethodArgs(methodName, args);
1468
+ validateMethodArgs(obj, methodName, args, g);
1433
1469
  if (Array.isArray(obj) && args.length === 1 && typeof args[0] === "function") {
1434
1470
  const asyncResult = await evalAsyncArrayMethod(methodName, obj, args[0]);
1435
1471
  if (asyncResult !== void 0) {
1436
1472
  g.checkTimeout();
1473
+ checkResultArrayLength(asyncResult.value, g);
1437
1474
  return asyncResult.value;
1438
1475
  }
1439
1476
  }
1440
- return await method.call(obj, ...args);
1477
+ const result = await method.call(obj, ...args);
1478
+ checkResultArrayLength(result, g);
1479
+ checkResultStringLength(result, g);
1480
+ return result;
1441
1481
  }
1442
1482
  ownDepth = false;
1443
1483
  g.exitDepth();
@@ -1614,6 +1654,28 @@ function createPluginRegistry() {
1614
1654
  //#endregion
1615
1655
  //#region src/index.ts
1616
1656
  const DEFAULT_CACHE_SIZE = 256;
1657
+ /**
1658
+ * Validate options at construction so misconfiguration fails fast with a clear
1659
+ * error rather than silently producing surprising behavior (e.g. a negative
1660
+ * cacheSize quietly disabling the cache).
1661
+ */
1662
+ function assertValidOptions(options) {
1663
+ const requireNonNegativeInt = (name, value) => {
1664
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) throw new RangeError(`bonsai: "${name}" must be a non-negative integer, received ${String(value)}`);
1665
+ };
1666
+ requireNonNegativeInt("cacheSize", options.cacheSize);
1667
+ requireNonNegativeInt("maxArrayLength", options.maxArrayLength);
1668
+ requireNonNegativeInt("maxStringLength", options.maxStringLength);
1669
+ if (options.maxDepth !== void 0 && (!Number.isInteger(options.maxDepth) || options.maxDepth < 1)) throw new RangeError(`bonsai: "maxDepth" must be a positive integer, received ${String(options.maxDepth)}`);
1670
+ if (options.timeout !== void 0 && (typeof options.timeout !== "number" || !Number.isFinite(options.timeout) || options.timeout < 0)) throw new RangeError(`bonsai: "timeout" must be a non-negative, finite number of milliseconds, received ${String(options.timeout)}`);
1671
+ const requireStringArray = (name, value) => {
1672
+ if (value === void 0) return;
1673
+ if (!Array.isArray(value)) throw new TypeError(`bonsai: "${name}" must be an array of strings`);
1674
+ if (!value.every((item) => typeof item === "string")) throw new TypeError(`bonsai: "${name}" must contain only strings`);
1675
+ };
1676
+ requireStringArray("allowedProperties", options.allowedProperties);
1677
+ requireStringArray("deniedProperties", options.deniedProperties);
1678
+ }
1617
1679
  let sharedInstance;
1618
1680
  /**
1619
1681
  * Evaluate a single expression with default options. Uses a shared instance internally.
@@ -1647,6 +1709,7 @@ function evaluateExpression(expression, context) {
1647
1709
  * ```
1648
1710
  */
1649
1711
  function bonsai(options = {}) {
1712
+ assertValidOptions(options);
1650
1713
  const registry = createPluginRegistry();
1651
1714
  const cache = new LRUCache(options.cacheSize ?? DEFAULT_CACHE_SIZE);
1652
1715
  const astCache = new LRUCache(options.cacheSize ?? DEFAULT_CACHE_SIZE);