bonsai-js 0.3.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-t5i9AEaf.mjs";
2
- import { i as tokenize, n as ExecutionContext, r as SecurityPolicy } from "./execution-context-DqYC3L92.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();
@@ -259,7 +288,7 @@ function parse(source, _depth = 0) {
259
288
  const tok = current();
260
289
  if (tok.type === "Number") {
261
290
  advance();
262
- const raw = tok.value.replace(/_/g, "");
291
+ const raw = tok.value.replace(/_/gu, "");
263
292
  return {
264
293
  type: "NumberLiteral",
265
294
  value: Number(raw),
@@ -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;
@@ -883,9 +878,16 @@ function resolveTransform(name, transforms) {
883
878
  if (!Object.hasOwn(transforms, name)) throw new BonsaiReferenceError("transform", name, suggest(name, Object.keys(transforms)));
884
879
  return transforms[name];
885
880
  }
886
- function resolveFunction(name, functions) {
887
- if (!Object.hasOwn(functions, name)) throw new BonsaiReferenceError("function", name, suggest(name, Object.keys(functions)));
888
- return functions[name];
881
+ /**
882
+ * Resolve a callable by name in the shared function namespace. Pure and context
883
+ * functions occupy one registry keyed by name, so a single lookup returns the
884
+ * tagged entry; the call site uses `kind` to decide how to invoke it (pure
885
+ * functions take only call args; context functions receive the evaluation
886
+ * context first, then call args).
887
+ */
888
+ function resolveCallable(name, functions) {
889
+ if (Object.hasOwn(functions, name)) return functions[name];
890
+ throw new BonsaiReferenceError("function", name, suggest(name, Object.keys(functions)));
889
891
  }
890
892
  function getIdentifierName(node, message = "Expected identifier") {
891
893
  if (node.type !== "Identifier") throw new Error(message);
@@ -914,7 +916,17 @@ function expandSpreadValue(value, maxLength) {
914
916
  }
915
917
  const REPLACE_METHODS = new Set(["replace", "replaceAll"]);
916
918
  const MAX_REPEAT_COUNT = 1e5;
917
- 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) {
918
930
  if (REPLACE_METHODS.has(methodName)) for (const arg of args) {
919
931
  if (typeof arg === "function") throw new BonsaiTypeError(methodName, "string arguments (callbacks are not allowed)", arg);
920
932
  if (arg instanceof RegExp) throw new BonsaiTypeError(methodName, "string arguments (RegExp is not allowed)", arg);
@@ -923,7 +935,32 @@ function validateMethodArgs(methodName, args) {
923
935
  if (methodName === "repeat") {
924
936
  const count = Number(args[0]);
925
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);
926
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);
927
964
  }
928
965
  function accessMember(object, propertyNode, computed, computedValue, guard) {
929
966
  const key = computed ? toPropertyKey(computedValue) : getIdentifierName(propertyNode, "Expected identifier property");
@@ -936,11 +973,11 @@ function rejectPromise(value, kind, name) {
936
973
  if (value instanceof Promise) throw new BonsaiTypeError(name, `a synchronous ${kind} result — use evaluate() instead of evaluateSync() for async`, value);
937
974
  return value;
938
975
  }
939
- function evaluate(node, context, transforms, functions, guard, source) {
976
+ function evaluate(node, context, bindings, guard, source) {
940
977
  return evalNode(node, {
941
978
  ctx: context,
942
- tr: transforms,
943
- fn: functions,
979
+ tr: bindings.transforms,
980
+ fn: bindings.functions,
944
981
  g: guard,
945
982
  s: source
946
983
  });
@@ -1054,18 +1091,21 @@ function evalCallExpression(node, env) {
1054
1091
  const method = validateMethodCall(obj, methodName, g);
1055
1092
  const args = [];
1056
1093
  for (const arg of node.args) pushCallArgument(args, arg, env);
1057
- validateMethodArgs(methodName, args);
1058
- 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;
1059
1099
  } catch (e) {
1060
1100
  if (s) attachLocation(e, s, node.start, node.end);
1061
1101
  throw e;
1062
1102
  }
1063
1103
  }
1064
1104
  if (node.callee.type === "Identifier") try {
1065
- const func = resolveFunction(node.callee.name, fn);
1105
+ const resolved = resolveCallable(node.callee.name, fn);
1066
1106
  const args = [];
1067
1107
  for (const arg of node.args) pushCallArgument(args, arg, env);
1068
- return rejectPromise(func(...args), "function", node.callee.name);
1108
+ return rejectPromise(resolved.kind === "context" ? resolved.fn(env.ctx, ...args) : resolved.fn(...args), "function", node.callee.name);
1069
1109
  } catch (e) {
1070
1110
  if (s) attachLocation(e, s, node.start, node.end);
1071
1111
  throw e;
@@ -1128,8 +1168,11 @@ function evalLambdaBody(node, item, env) {
1128
1168
  const args = [];
1129
1169
  for (const arg of node.args) if (arg.type === "SpreadElement") args.push(...expandSpreadValue(evalNode(arg.argument, env), g.policy.maxArrayLength));
1130
1170
  else args.push(evalArg(arg, env));
1131
- validateMethodArgs(methodName, args);
1132
- 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;
1133
1176
  }
1134
1177
  ownDepth = false;
1135
1178
  g.exitDepth();
@@ -1175,11 +1218,11 @@ function makeLambdaAccessor$1(property, guard) {
1175
1218
  }
1176
1219
  //#endregion
1177
1220
  //#region src/evaluator-async.ts
1178
- async function evaluateAsync(node, context, transforms, functions, guard, source) {
1221
+ async function evaluateAsync(node, context, bindings, guard, source) {
1179
1222
  const result = await evalNodeAsync(node, {
1180
1223
  ctx: context,
1181
- tr: transforms,
1182
- fn: functions,
1224
+ tr: bindings.transforms,
1225
+ fn: bindings.functions,
1183
1226
  g: guard,
1184
1227
  s: source
1185
1228
  });
@@ -1295,18 +1338,21 @@ async function evalCallExpressionAsync(node, env) {
1295
1338
  const method = validateMethodCall(obj, methodName, g);
1296
1339
  const args = [];
1297
1340
  for (const arg of node.args) await pushCallArgumentAsync(args, arg, env);
1298
- validateMethodArgs(methodName, args);
1341
+ validateMethodArgs(obj, methodName, args, g);
1299
1342
  if (Array.isArray(obj) && args.length === 1 && typeof args[0] === "function") {
1300
1343
  const arr = obj;
1301
1344
  const predicate = args[0];
1302
1345
  const asyncResult = await evalAsyncArrayMethod(methodName, arr, predicate);
1303
1346
  if (asyncResult !== void 0) {
1304
1347
  g.checkTimeout();
1348
+ checkResultArrayLength(asyncResult.value, g);
1305
1349
  return asyncResult.value;
1306
1350
  }
1307
1351
  }
1308
1352
  const result = await method.call(obj, ...args);
1309
1353
  g.checkTimeout();
1354
+ checkResultArrayLength(result, g);
1355
+ checkResultStringLength(result, g);
1310
1356
  return result;
1311
1357
  } catch (e) {
1312
1358
  if (s) attachLocation(e, s, node.start, node.end);
@@ -1314,10 +1360,10 @@ async function evalCallExpressionAsync(node, env) {
1314
1360
  }
1315
1361
  }
1316
1362
  if (node.callee.type === "Identifier") try {
1317
- const func = resolveFunction(node.callee.name, fn);
1363
+ const resolved = resolveCallable(node.callee.name, fn);
1318
1364
  const args = [];
1319
1365
  for (const arg of node.args) await pushCallArgumentAsync(args, arg, env);
1320
- const result = await func(...args);
1366
+ const result = resolved.kind === "context" ? await resolved.fn(env.ctx, ...args) : await resolved.fn(...args);
1321
1367
  g.checkTimeout();
1322
1368
  return result;
1323
1369
  } catch (e) {
@@ -1336,35 +1382,32 @@ async function pushCallArgumentAsync(args, node, env) {
1336
1382
  async function evalAsyncArrayMethod(methodName, arr, predicate) {
1337
1383
  switch (methodName) {
1338
1384
  case "filter": {
1339
- const results = arr.map(predicate);
1340
- const resolved = results.some((r) => r instanceof Promise) ? await Promise.all(results) : results;
1341
- 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 };
1342
1388
  }
1343
1389
  case "map": {
1344
- const results = arr.map(predicate);
1345
- return { value: results.some((r) => r instanceof Promise) ? await Promise.all(results) : results };
1346
- }
1347
- case "find": {
1348
- const results = arr.map(predicate);
1349
- const idx = (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).findIndex(Boolean);
1350
- return { value: idx >= 0 ? arr[idx] : void 0 };
1351
- }
1352
- case "some": {
1353
- const results = arr.map(predicate);
1354
- return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).some(Boolean) };
1355
- }
1356
- case "every": {
1357
- const results = arr.map(predicate);
1358
- return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).every(Boolean) };
1359
- }
1360
- case "findIndex": {
1361
- const results = arr.map(predicate);
1362
- 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 };
1363
1393
  }
1364
1394
  case "flatMap": {
1365
- const results = arr.map(predicate);
1366
- 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() };
1367
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 };
1368
1411
  default: return;
1369
1412
  }
1370
1413
  }
@@ -1422,15 +1465,19 @@ async function evalLambdaBodyAsync(node, item, env) {
1422
1465
  const args = [];
1423
1466
  for (const arg of node.args) if (arg.type === "SpreadElement") args.push(...expandSpreadValue(await evalNodeAsync(arg.argument, env), g.policy.maxArrayLength));
1424
1467
  else args.push(await evalArgAsync(arg, env));
1425
- validateMethodArgs(methodName, args);
1468
+ validateMethodArgs(obj, methodName, args, g);
1426
1469
  if (Array.isArray(obj) && args.length === 1 && typeof args[0] === "function") {
1427
1470
  const asyncResult = await evalAsyncArrayMethod(methodName, obj, args[0]);
1428
1471
  if (asyncResult !== void 0) {
1429
1472
  g.checkTimeout();
1473
+ checkResultArrayLength(asyncResult.value, g);
1430
1474
  return asyncResult.value;
1431
1475
  }
1432
1476
  }
1433
- 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;
1434
1481
  }
1435
1482
  ownDepth = false;
1436
1483
  g.exitDepth();
@@ -1527,13 +1574,24 @@ function createPluginRegistry() {
1527
1574
  let functionsCache = {};
1528
1575
  let transformsDirty = false;
1529
1576
  let functionsDirty = false;
1577
+ let bindingsCache;
1530
1578
  const registry = {
1531
1579
  addTransform(name, fn) {
1532
1580
  transformMap.set(name, fn);
1533
1581
  transformsDirty = true;
1534
1582
  },
1535
1583
  addFunction(name, fn) {
1536
- functionMap.set(name, fn);
1584
+ functionMap.set(name, {
1585
+ kind: "pure",
1586
+ fn
1587
+ });
1588
+ functionsDirty = true;
1589
+ },
1590
+ addContextFunction(name, fn) {
1591
+ functionMap.set(name, {
1592
+ kind: "context",
1593
+ fn
1594
+ });
1537
1595
  functionsDirty = true;
1538
1596
  },
1539
1597
  removeTransform(name) {
@@ -1552,6 +1610,12 @@ function createPluginRegistry() {
1552
1610
  getFunction(name) {
1553
1611
  return functionMap.get(name);
1554
1612
  },
1613
+ isContextFunction(name) {
1614
+ return functionMap.get(name)?.kind === "context";
1615
+ },
1616
+ hasFunction(name) {
1617
+ return functionMap.has(name);
1618
+ },
1555
1619
  getTransformNames() {
1556
1620
  return [...transformMap.keys()];
1557
1621
  },
@@ -1574,6 +1638,15 @@ function createPluginRegistry() {
1574
1638
  functionsDirty = false;
1575
1639
  }
1576
1640
  return functionsCache;
1641
+ },
1642
+ get bindings() {
1643
+ const t = registry.transforms;
1644
+ const f = registry.functions;
1645
+ if (!bindingsCache || bindingsCache.transforms !== t || bindingsCache.functions !== f) bindingsCache = {
1646
+ transforms: t,
1647
+ functions: f
1648
+ };
1649
+ return bindingsCache;
1577
1650
  }
1578
1651
  };
1579
1652
  return registry;
@@ -1581,27 +1654,62 @@ function createPluginRegistry() {
1581
1654
  //#endregion
1582
1655
  //#region src/index.ts
1583
1656
  const DEFAULT_CACHE_SIZE = 256;
1584
- let _shared;
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
+ }
1679
+ let sharedInstance;
1585
1680
  /**
1586
1681
  * Evaluate a single expression with default options. Uses a shared instance internally.
1587
1682
  * For repeated evaluation or custom configuration, use `bonsai()` to create a dedicated instance.
1588
1683
  */
1589
1684
  function evaluateExpression(expression, context) {
1590
- if (!_shared) _shared = bonsai();
1591
- return _shared.evaluateSync(expression, context);
1685
+ if (!sharedInstance) sharedInstance = bonsai();
1686
+ return sharedInstance.evaluateSync(expression, context);
1592
1687
  }
1593
1688
  /**
1594
1689
  * Create a new Bonsai instance with optional safety and caching configuration.
1595
- * Register transforms and functions via `.use()`, `.addTransform()`, and `.addFunction()`.
1690
+ * Register transforms and functions via `.use()`, `.addTransform()`, `.addFunction()`,
1691
+ * and `.addContextFunction()`.
1692
+ *
1693
+ * Pass a context type argument to get end-to-end type safety on the evaluation
1694
+ * context: `bonsai<MyContext>()` will type-check `evaluate(expr, ctx)` calls,
1695
+ * require a context argument when `MyContext` has required fields, and
1696
+ * propagate `MyContext` into context-aware function signatures.
1596
1697
  *
1597
1698
  * @example
1598
1699
  * ```ts
1599
1700
  * const expr = bonsai({ timeout: 50 })
1600
1701
  * expr.use(strings)
1601
1702
  * expr.evaluateSync('name |> trim |> upper', { name: ' hello ' }) // "HELLO"
1703
+ *
1704
+ * type AppCtx = { userId: string; perms: string[] }
1705
+ * const app = bonsai<AppCtx>()
1706
+ * app.addContextFunction('hasPermission', (ctx, action) =>
1707
+ * ctx.perms.includes(String(action)))
1708
+ * app.evaluateSync('hasPermission("write")', { userId: 'u_1', perms: ['write'] })
1602
1709
  * ```
1603
1710
  */
1604
1711
  function bonsai(options = {}) {
1712
+ assertValidOptions(options);
1605
1713
  const registry = createPluginRegistry();
1606
1714
  const cache = new LRUCache(options.cacheSize ?? DEFAULT_CACHE_SIZE);
1607
1715
  const astCache = new LRUCache(options.cacheSize ?? DEFAULT_CACHE_SIZE);
@@ -1625,15 +1733,16 @@ function bonsai(options = {}) {
1625
1733
  const compiled = {
1626
1734
  ast: optimized,
1627
1735
  source,
1628
- async evaluate(context = {}) {
1629
- return evaluateAsync(optimized, context, registry.transforms, registry.functions, createExecutionContext(), source);
1736
+ async evaluate(...args) {
1737
+ return evaluateAsync(optimized, args[0] ?? {}, registry.bindings, createExecutionContext(), source);
1630
1738
  },
1631
- evaluateSync(context = {}) {
1632
- if (syncCtxInUse) return evaluate(optimized, context, registry.transforms, registry.functions, createExecutionContext(), source);
1739
+ evaluateSync(...args) {
1740
+ const ctx = args[0] ?? {};
1741
+ if (syncCtxInUse) return evaluate(optimized, ctx, registry.bindings, createExecutionContext(), source);
1633
1742
  syncCtxInUse = true;
1634
1743
  try {
1635
1744
  syncCtx.reset();
1636
- return evaluate(optimized, context, registry.transforms, registry.functions, syncCtx, source);
1745
+ return evaluate(optimized, ctx, registry.bindings, syncCtx, source);
1637
1746
  } finally {
1638
1747
  syncCtxInUse = false;
1639
1748
  }
@@ -1655,6 +1764,10 @@ function bonsai(options = {}) {
1655
1764
  registry.addFunction(name, fn);
1656
1765
  return instance;
1657
1766
  },
1767
+ addContextFunction(name, fn) {
1768
+ registry.addContextFunction(name, fn);
1769
+ return instance;
1770
+ },
1658
1771
  removeTransform(name) {
1659
1772
  return registry.removeTransform(name);
1660
1773
  },
@@ -1665,7 +1778,10 @@ function bonsai(options = {}) {
1665
1778
  return registry.getTransform(name) !== void 0;
1666
1779
  },
1667
1780
  hasFunction(name) {
1668
- return registry.getFunction(name) !== void 0;
1781
+ return registry.hasFunction(name);
1782
+ },
1783
+ isContextFunction(name) {
1784
+ return registry.isContextFunction(name);
1669
1785
  },
1670
1786
  listTransforms() {
1671
1787
  return registry.getTransformNames();
@@ -1686,16 +1802,17 @@ function bonsai(options = {}) {
1686
1802
  compile(expression) {
1687
1803
  return compileExpr(expression);
1688
1804
  },
1689
- async evaluate(expression, context = {}) {
1690
- return evaluateAsync(getAst(expression), context, registry.transforms, registry.functions, createExecutionContext(), expression);
1805
+ async evaluate(expression, ...args) {
1806
+ return evaluateAsync(getAst(expression), args[0] ?? {}, registry.bindings, createExecutionContext(), expression);
1691
1807
  },
1692
- evaluateSync(expression, context = {}) {
1808
+ evaluateSync(expression, ...args) {
1693
1809
  const ast = getAst(expression);
1694
- if (syncCtxInUse) return evaluate(ast, context, registry.transforms, registry.functions, createExecutionContext(), expression);
1810
+ const ctx = args[0] ?? {};
1811
+ if (syncCtxInUse) return evaluate(ast, ctx, registry.bindings, createExecutionContext(), expression);
1695
1812
  syncCtxInUse = true;
1696
1813
  try {
1697
1814
  syncCtx.reset();
1698
- return evaluate(ast, context, registry.transforms, registry.functions, syncCtx, expression);
1815
+ return evaluate(ast, ctx, registry.bindings, syncCtx, expression);
1699
1816
  } finally {
1700
1817
  syncCtxInUse = false;
1701
1818
  }