@enclave-vm/ast
v2.17.0
Published
A production-ready AST security guard for JavaScript - validate, protect, and enforce code safety with extensible rules
Maintainers
Readme
AST Guard
A production-ready, extensible AST validator for JavaScript with rule-based validation
AST Guard is a powerful static analysis tool for JavaScript code. It provides a flexible, rule-based architecture for validating Abstract Syntax Trees (AST) with comprehensive built-in rules and support for custom validation logic.
Bank-Grade Security
Hardened Against All Known Sandbox Escape Exploits
| Metric | Value | | -------------- | ---------------------------------------------------- | | CVE Protection | 100% (all known vm2, isolated-vm, node-vm exploits) | | Security Tests | 613 tests, 100% pass rate | | Code Coverage | 95%+ | | Defense Layers | 4 (AST validation, transformation, proxy, isolation) |
Protected Against:
- vm2 CVEs: CVE-2023-29017, CVE-2023-30547, CVE-2023-32313, CVE-2023-37466
- Constructor chain escapes:
[].constructor.constructor('code')() - Prototype pollution:
__proto__,Object.setPrototypeOf - Reflection API bypasses:
Reflect.get,Reflect.construct - Global object access:
window,globalThis,this
For detailed CVE analysis, see CVE-COVERAGE.md. For the full list of 100+ blocked attack vectors, see SECURITY-AUDIT.md.
Features
- Production-Ready: Comprehensive error handling, type-safe APIs, and battle-tested rules
- Extensible: Plugin architecture with custom rule support
- Type-Safe: Full TypeScript support with strict typing
- Zero Dependencies: Lightweight with only
acornandacorn-walkas dependencies - Comprehensive Rules: Built-in rules for common validation scenarios
- Security-Focused: Rules to prevent dangerous code patterns
- Performance: Fast validation with configurable limits
- Detailed Reporting: Rich error messages with source locations
Installation
npm install ast-guard
# or
yarn add ast-guard
# or
pnpm add ast-guardQuick Start
import { JSAstValidator, DisallowedIdentifierRule, NoEvalRule } from 'ast-guard';
// Create validator with built-in rules
const validator = new JSAstValidator([
new DisallowedIdentifierRule({ disallowed: ['eval', 'Function'] }),
new NoEvalRule(),
]);
// Validate code
const result = await validator.validate(`
const x = 1;
eval("alert('hello')");
`);
if (!result.valid) {
console.log('Validation failed:');
result.issues.forEach((issue) => {
console.log(`- [${issue.severity}] ${issue.message} (${issue.code})`);
if (issue.location) {
console.log(` at line ${issue.location.line}, column ${issue.location.column}`);
}
});
}Presets
AST Guard provides pre-configured security presets for quick and easy setup. Choose from five security levels, from maximum lockdown to minimal restrictions.
Available Presets
| Preset | Security Level | Description | Use Case | | -------------- | -------------- | ---------------------------------------------------- | ----------------------------------- | | STRICT | Maximum | Blocks all dangerous patterns, loops, and async | Untrusted code execution | | SECURE | High | Blocks most dangerous patterns, allows bounded loops | Semi-trusted code with restrictions | | STANDARD | Medium | Sensible defaults, blocks critical risks only | Trusted code with safety checks | | PERMISSIVE | Low | Minimal restrictions, only blocks eval | Internal scripts |
Quick Start with Presets
import { JSAstValidator, Presets, PresetLevel } from 'ast-guard';
// Option 1: Use the Presets object
const lockdownRules = Presets.strict();
const validator = new JSAstValidator(lockdownRules);
// Option 2: Use createPreset with level
import { createPreset } from 'ast-guard';
const securedRules = createPreset(PresetLevel.SECURE);
const validator = new JSAstValidator(securedRules);
// Option 3: Use individual preset function
import { createStandardPreset } from 'ast-guard';
const balancedRules = createStandardPreset();
const validator = new JSAstValidator(balancedRules);Preset Details
STRICT Preset
Maximum security for untrusted code. Bank-grade protection.
Includes all security rules to block known sandbox escape exploits:
- ✅ NoEvalRule - Blocks eval() and Function constructor
- ✅ NoGlobalAccessRule - Blocks constructor chains, Reflect API, global object access
- ✅ NoAsyncRule - Blocks async/await patterns
- ✅ DisallowedIdentifierRule - Blocks dangerous identifiers
- ✅ ForbiddenLoopRule - Blocks loops (or optionally allow bounded loops)
- ✅ And more...
import { Presets } from 'ast-guard';
// Maximum lockdown
const rules = Presets.strict();
// With customization
const rules = Presets.strict({
// Allow specific loops
allowedLoops: { allowFor: true, allowForOf: true },
// Require specific API calls
requiredFunctions: ['callTool'],
minFunctionCalls: 1,
maxFunctionCalls: 10,
// Validate function arguments
functionArgumentRules: {
callTool: {
minArgs: 2,
expectedTypes: ['string', 'object'],
},
},
// Add custom blocked identifiers
additionalDisallowedIdentifiers: ['window', 'document'],
});Blocks:
- All eval-like constructs (
eval,Function,setTimeoutwith strings) - All loops (for, while, do-while, for-in, for-of)
- All async/await
- Dangerous identifiers:
process,require,global,__dirname,constructor,__proto__, etc.
SECURE Preset
High security with some flexibility for semi-trusted code.
import { Presets } from 'ast-guard';
const rules = Presets.secured({
requiredFunctions: ['callTool'],
maxFunctionCalls: 20,
});Blocks:
- All eval-like constructs
- Dangerous identifiers:
process,require,global,__dirname - Infinite loops (while, do-while)
- Async function declarations
Allows:
- Bounded for/for-of loops
- Await expressions (but not async functions)
STANDARD Preset
Medium security with sensible defaults for most use cases.
import { Presets } from 'ast-guard';
const rules = Presets.balanced({
requiredFunctions: ['callTool'],
additionalDisallowedIdentifiers: ['window', 'document'], // Browser-specific
});Blocks:
- eval() and Function constructor
- Critical identifiers:
process,require,eval,Function - Infinite while/do-while loops
Allows:
- All for loops (for, for-in, for-of)
- Async/await
- Constructor and prototype access
PERMISSIVE Preset
Low security for internal or trusted scripts.
import { Presets } from 'ast-guard';
const rules = Presets.naive({
requiredFunctions: ['callTool'], // Optional API enforcement
});Blocks:
- eval() only
Allows:
- All loops
- Async/await
- All identifiers
- Constructor and prototype access
Customizing Presets
All presets accept options to customize their behavior:
import { Presets } from 'ast-guard';
const rules = Presets.secured({
// Add more blocked identifiers
additionalDisallowedIdentifiers: ['window', 'document', 'localStorage'],
// Require specific functions
requiredFunctions: ['callTool', 'getTool'],
minFunctionCalls: 1,
maxFunctionCalls: 50,
// Override default loop restrictions
allowedLoops: {
allowFor: true,
allowWhile: true, // Override: allow while in secured preset
allowDoWhile: false,
allowForIn: false,
allowForOf: true,
},
// Override default async restrictions
allowAsync: {
allowAsyncFunctions: true, // Override: allow async in secured preset
allowAwait: true,
},
// Validate specific function arguments
functionArgumentRules: {
callTool: {
minArgs: 2,
maxArgs: 3,
expectedTypes: ['string', 'object'],
validator: (args) => {
// Custom validation
if (args[0]?.value === '') {
return 'Tool name cannot be empty';
}
return null;
},
},
},
});Real-World Examples
Example 1: Untrusted Code Sandbox
import { JSAstValidator, Presets } from 'ast-guard';
// Maximum security for untrusted user code
const validator = new JSAstValidator(
Presets.strict({
requiredFunctions: ['callTool'],
functionArgumentRules: {
callTool: {
minArgs: 2,
expectedTypes: ['string', 'object'],
},
},
}),
);
const userCode = `
// User-submitted code
callTool("getData", { id: 123 });
`;
const result = await validator.validate(userCode, {
rules: {
'no-eval': true,
'disallowed-identifier': true,
'forbidden-loop': true,
'required-function-call': true,
'call-argument-validation': true,
},
});
if (result.valid) {
// Safe to execute in sandbox
executeInSandbox(userCode);
}Example 2: Plugin System
import { JSAstValidator, Presets } from 'ast-guard';
// Balanced security for plugin code
const validator = new JSAstValidator(
Presets.balanced({
requiredFunctions: ['registerPlugin'],
additionalDisallowedIdentifiers: ['process', 'require', 'fs'],
}),
);
const pluginCode = await loadPluginCode();
const result = await validator.validate(pluginCode, {
rules: {
'no-eval': true,
'disallowed-identifier': true,
'required-function-call': true,
},
});Example 3: Internal Automation Scripts
import { JSAstValidator, Presets } from 'ast-guard';
// Minimal security for trusted internal scripts
const validator = new JSAstValidator(
Presets.naive({
requiredFunctions: ['execute'],
}),
);Preset Comparison Matrix
| Feature | STRICT | SECURE | STANDARD | PERMISSIVE | | -------------------- | ------ | ------ | -------- | ---------- | | eval() | ❌ | ❌ | ❌ | ❌ | | Function() | ❌ | ❌ | ❌ | ✅ | | for loops | ❌ | ✅ | ✅ | ✅ | | while loops | ❌ | ❌ | ❌ | ✅ | | async/await | ❌ | ⚠️ | ✅ | ✅ | | process | ❌ | ❌ | ❌ | ✅ | | require | ❌ | ❌ | ❌ | ✅ | | global | ❌ | ❌ | ✅ | ✅ | | constructor | ❌ | ✅ | ✅ | ✅ | | Unreachable code | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
Legend: ❌ Blocked (error) | ⚠️ Detected (warning) | ✅ Allowed
AgentScript Globals by Security Level
The AgentScript preset uses security-level-aware globals for defense-in-depth:
| Global | STRICT | SECURE | STANDARD | PERMISSIVE |
| ------------------------------------------ | ------ | ------ | -------- | ---------- |
| Core API | | | | |
| callTool | ✅ | ✅ | ✅ | ✅ |
| Data Types | | | | |
| Math, JSON, Array | ✅ | ✅ | ✅ | ✅ |
| Object, String, Number, Date | ✅ | ✅ | ✅ | ✅ |
| Constants | | | | |
| undefined, NaN, Infinity | ✅ | ✅ | ✅ | ✅ |
| Utility Functions | | | | |
| parseInt, parseFloat | ❌ | ✅ | ✅ | ✅ |
| isNaN, isFinite | ❌ | ✅ | ✅ | ✅ |
| encodeURI, decodeURI | ❌ | ✅ | ✅ | ✅ |
| encodeURIComponent, decodeURIComponent | ❌ | ✅ | ✅ | ✅ |
| Debugging | | | | |
| console | ❌ | ❌ | ❌ | ✅ |
Always Blocked: eval, Function, process, require, window, globalThis, constructor, __proto__, Proxy, Reflect, Promise, setTimeout, fetch, WebSocket, Map, Set, Symbol, RegExp, and 50+ other dangerous globals. The full list is exported (frozen) as AGENTSCRIPT_DISALLOWED_IDENTIFIERS.
Tool Namespaces
normalizeToolNamespaces(spec, { reservedNames }) validates the toolNamespaces option of @enclave-vm/core
and @enclave-vm/browser ({ mail: ['list', 'send'] } or { users: { get: 'users:get' } }) and returns it as
plain data, so every runtime refuses the same names: non-identifiers, prototype keys, names starting with __,
reserved words and sandbox globals as namespaces, identifiers AgentScript refuses (as namespaces or method
names), collisions with reservedNames, duplicated methods, and tool names that are empty, oversized or not
valid tool names (TOOL_NAMESPACE_TOOL_NAME_PATTERN: letters, digits, :, ., _ and -, starting with a
letter, as the worker_threads and iframe protocols require; this also applies to the generated
<namespace>.<method> names, so _util or $db need explicit tool names). It throws a TypeError whose
message starts with Invalid toolNamespaces:.
AgentScript Preset
The AgentScript preset is specifically designed for safe AI agent orchestration. It provides security rules tailored for executing LLM-generated code that calls tools via callTool().
import { JSAstValidator, createAgentScriptPreset } from 'ast-guard';
// Basic usage - secure defaults for AgentScript
const rules = createAgentScriptPreset();
const validator = new JSAstValidator(rules);
// With requireCallTool - enforce that code must call tools
const rules = createAgentScriptPreset({
requireCallTool: true, // Require at least one callTool() invocation
});Options:
| Option | Type | Default | Description |
| --------------------------------- | ---------- | -------------------------- | -------------------------------------------------------------- |
| securityLevel | string | 'STANDARD' | Security level: STRICT, SECURE, STANDARD, PERMISSIVE |
| requireCallTool | boolean | false | Require at least one callTool() invocation in the code |
| allowedGlobals | string[] | (based on securityLevel) | Override allowed globals (takes precedence over securityLevel) |
| additionalDisallowedIdentifiers | string[] | [] | Additional identifiers to block beyond the default set |
| allowArrowFunctions | boolean | true | Whether to allow arrow functions (for array methods) |
| allowedLoops | object | { allowFor, allowForOf } | Override default loop restrictions |
| reservedPrefixes | string[] | ['__ag_', '__safe_'] | Reserved prefixes that user code cannot use |
| staticCallTarget | object | { enabled: true } | Configuration for static call target validation |
| callToolValidation | object | - | Validation rules for callTool arguments |
Example with all options:
const rules = createAgentScriptPreset({
// Require the code to actually call tools
requireCallTool: true,
// Customize allowed globals
allowedGlobals: ['callTool', 'getTool', 'Math', 'JSON', 'Array', 'Object'],
// Block additional identifiers
additionalDisallowedIdentifiers: ['fetch', 'XMLHttpRequest'],
// Allow arrow functions for array methods (default: true)
allowArrowFunctions: true,
// Allow specific loops (default: for and for-of allowed)
allowedLoops: {
allowFor: true,
allowForOf: true,
allowWhile: false, // Block while loops
allowDoWhile: false, // Block do-while loops
allowForIn: false, // Block for-in loops
},
// Static call target validation
staticCallTarget: {
enabled: true,
allowedToolNames: ['users:list', 'users:get'], // Optional whitelist
},
});The AgentScript preset is used internally by the enclave-vm package for secure code execution.
Built-in Rules
DisallowedIdentifierRule
Prevents usage of specific identifiers (e.g., eval, Function, process).
import { DisallowedIdentifierRule } from 'ast-guard';
const rule = new DisallowedIdentifierRule({
disallowed: ['eval', 'Function', 'process', 'require'],
messageTemplate: 'Access to "{identifier}" is forbidden',
});ForbiddenLoopRule
Prevents usage of loop constructs. Configurable to allow specific loop types.
import { ForbiddenLoopRule } from 'ast-guard';
const rule = new ForbiddenLoopRule({
allowFor: false,
allowWhile: false,
allowDoWhile: false,
allowForOf: true, // Allow for-of loops
allowForIn: false,
message: 'Loops are not allowed in this context',
});RequiredFunctionCallRule
Ensures specific functions are called at least once.
import { RequiredFunctionCallRule } from 'ast-guard';
const rule = new RequiredFunctionCallRule({
required: ['callTool'],
minCalls: 1,
maxCalls: 10,
messageTemplate: 'Must call {function} at least once',
});Mode Options:
mode: 'all'(default) - All functions inrequiredmust be calledmode: 'any'- At least one function fromrequiredmust be called
// Require either callTool or invokeAPI to be called
const rule = new RequiredFunctionCallRule({
required: ['callTool', 'invokeAPI'],
mode: 'any',
minCalls: 1,
});UnreachableCodeRule
Detects unreachable code (code after return/throw/break/continue).
import { UnreachableCodeRule } from 'ast-guard';
const rule = new UnreachableCodeRule();
// Automatically detects unreachable codeCallArgumentValidationRule
Validates function call arguments (count and types).
import { CallArgumentValidationRule } from 'ast-guard';
const rule = new CallArgumentValidationRule({
functions: {
callTool: {
minArgs: 2,
maxArgs: 2,
expectedTypes: ['string', 'object'],
},
getTool: {
minArgs: 1,
maxArgs: 1,
expectedTypes: ['string'],
validator: (args, node) => {
// Custom validation logic
const firstArg = args[0];
if (firstArg.type === 'Literal' && firstArg.value === '') {
return 'Tool name cannot be empty';
}
return null; // Valid
},
},
},
});NoEvalRule
Prevents usage of eval(), Function() constructor, and string-based setTimeout/setInterval.
import { NoEvalRule } from 'ast-guard';
const rule = new NoEvalRule();
// Blocks: eval(), new Function(), setTimeout("code", ...)NoAsyncRule
Prevents usage of async/await constructs.
import { NoAsyncRule } from 'ast-guard';
const rule = new NoAsyncRule({
allowAsyncFunctions: false,
allowAwait: false,
message: 'Async code is not supported',
});Custom Rules
Create your own validation rules by implementing the ValidationRule interface:
import { ValidationRule, ValidationContext, ValidationSeverity } from 'ast-guard';
import * as walk from 'acorn-walk';
class NoConsoleRule implements ValidationRule {
readonly name = 'no-console';
readonly description = 'Prevents usage of console methods';
readonly defaultSeverity = ValidationSeverity.WARNING;
readonly enabledByDefault = true;
validate(context: ValidationContext): void {
walk.simple(context.ast as any, {
MemberExpression: (node: any) => {
if (node.object.type === 'Identifier' && node.object.name === 'console') {
context.report({
code: 'NO_CONSOLE',
message: 'Console usage is not recommended',
location: node.loc
? {
line: node.loc.start.line,
column: node.loc.start.column,
}
: undefined,
});
}
},
});
}
}
// Use your custom rule
const validator = new JSAstValidator([new NoConsoleRule()]);Configuration
Validator Configuration
const result = await validator.validate(code, {
// Parse options (passed to acorn)
parseOptions: {
ecmaVersion: 'latest',
sourceType: 'script',
},
// Rule configuration
rules: {
'disallowed-identifier': true, // Enable with default options
'no-eval': false, // Disable rule
'required-function-call': {
// Enable with custom options
enabled: true,
severity: ValidationSeverity.ERROR,
options: { required: ['callTool'] },
},
},
// Stop on first error
stopOnFirstError: false,
// Maximum number of issues (0 = unlimited)
maxIssues: 100,
});Rule Management
const validator = new JSAstValidator();
// Register rules
validator.registerRule(new DisallowedIdentifierRule({ disallowed: ['eval'] }));
validator.registerRule(new NoEvalRule());
// Get all rules
const rules = validator.getRules();
// Get specific rule
const rule = validator.getRule('no-eval');
// Unregister rule
validator.unregisterRule('no-eval');Validation Results
interface ValidationResult {
valid: boolean; // true if no errors
issues: ValidationIssue[]; // All issues found
ast?: acorn.Node; // Parsed AST (if parsing succeeded)
parseError?: Error; // Parse error (if parsing failed)
}
interface ValidationIssue {
code: string; // Unique issue code
severity: ValidationSeverity; // ERROR | WARNING | INFO
message: string; // Human-readable message
location?: SourceLocation; // Source location
data?: Record<string, unknown>; // Additional context
}Error Handling
All errors extend JSAstValidatorError with specific error types:
import {
JSAstValidatorError,
ParseError,
RuleConfigurationError,
ConfigurationError,
RuleNotFoundError,
InvalidSourceError,
} from 'ast-guard';
try {
const result = await validator.validate(code);
} catch (error) {
if (error instanceof ParseError) {
console.error(`Parse error at line ${error.line}:`, error.message);
} else if (error instanceof InvalidSourceError) {
console.error('Invalid source:', error.message);
} else if (error instanceof RuleConfigurationError) {
console.error(`Rule ${error.ruleName} misconfigured:`, error.message);
}
}Statistics
const result = await validator.validate(code);
const stats = validator.getStats(result, Date.now() - startTime);
console.log(`Total issues: ${stats.totalIssues}`);
console.log(`Errors: ${stats.errors}`);
console.log(`Warnings: ${stats.warnings}`);
console.log(`Duration: ${stats.durationMs}ms`);Use Cases
1. Sandboxed Code Execution
Validate code before executing in a sandboxed environment:
const validator = new JSAstValidator([
new DisallowedIdentifierRule({ disallowed: ['eval', 'Function', 'process'] }),
new NoEvalRule(),
new ForbiddenLoopRule(), // Prevent infinite loops
new RequiredFunctionCallRule({ required: ['callTool'] }),
]);
const result = await validator.validate(userCode);
if (result.valid) {
// Safe to execute
vm.runInContext(userCode, sandbox);
}2. API Contract Validation
Ensure code follows API contracts:
const validator = new JSAstValidator([
new RequiredFunctionCallRule({ required: ['callTool'] }),
new CallArgumentValidationRule({
functions: {
callTool: {
minArgs: 2,
expectedTypes: ['string', 'object'],
},
},
}),
]);3. Security Analysis
Detect potentially dangerous code patterns:
const validator = new JSAstValidator([
new NoEvalRule(),
new DisallowedIdentifierRule({
disallowed: ['eval', 'Function', 'require', 'process', 'child_process'],
}),
]);4. Code Quality Checks
Detect code quality issues:
const validator = new JSAstValidator([
new UnreachableCodeRule(),
// Add custom rules for your quality standards
]);Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coveragePerformance
- Parse time: ~1-5ms for typical scripts (depends on size)
- Validation time: ~1-10ms per rule (depends on complexity)
- Memory: Minimal overhead, AST is shared across rules
ReDoS Analysis Without the Parser
To check regular expressions for catastrophic backtracking without validating JavaScript, import the analyzer from @enclave-vm/ast/redos. It loads none of the parser (acorn, acorn-walk, astring) that the package root loads: about 8 KB instead of 240 KB plus the parser.
import { analyzeForReDoS, REDOS_THRESHOLDS } from '@enclave-vm/ast/redos';
const { score } = analyzeForReDoS('(a+)+$', 'catastrophic');
if (score >= REDOS_THRESHOLDS.BLOCK) throw new Error('Pattern can backtrack catastrophically');It also exports REDOS_PATTERNS, calculateStarHeight and the ReDoSAnalysisResult type, the same as the package root.
Comparison
| Feature | AST Guard | ESLint | TypeScript | | ---------------------- | -------------- | ------------ | ------------- | | Focus | AST validation | Code linting | Type checking | | Extensible | ✅ | ✅ | ⚠️ | | Lightweight | ✅ | ❌ | ❌ | | Type-safe | ✅ | ⚠️ | ✅ | | Security rules | ✅ | ⚠️ | ❌ | | Runtime validation | ✅ | ❌ | ❌ |
Defense-in-Depth Architecture
AST Guard provides 4 layers of protection:
Layer 1: AST Validation
├── NoEvalRule - Blocks eval(), Function()
├── NoGlobalAccessRule - Blocks window, globalThis, .constructor
├── NoAsyncRule - Blocks async/await (optional)
├── DisallowedIdentifierRule - Blocks dangerous identifiers
└── ForbiddenLoopRule - Blocks/transforms loops
Layer 2: AST Transformation
├── Direct identifiers: console → __safe_console
├── Computed access: obj['eval'] → obj['__safe_eval']
└── Static string literals transformed
Layer 3: Runtime Proxy Layer
├── __safe_callTool() - Validates tool calls
├── __safe_console() - Captures logs
└── All proxied functions enforce security
Layer 4: Worker Isolation
├── Separate worker thread
├── Sandboxed VM context
└── No access to Node.js globalsKnown Limitations
These are inherent to any static analyzer (not vulnerabilities):
| Limitation | Example | Mitigation |
| ---------------------------- | -------------------- | --------------------------------- |
| Computed property access | obj['constructor'] | Object.freeze(Object.prototype) |
| Runtime string construction | 'con' + 'structor' | VM isolation |
| Destructuring property names | { constructor: c } | Freeze prototypes |
Documentation
| Document | Purpose | | ------------------------------------------------------------ | ----------------------------------------------- | | docs/AGENTSCRIPT.md | AgentScript language reference for AI agents | | docs/CVE-COVERAGE.md | Detailed CVE analysis and protection mechanisms | | docs/SECURITY-AUDIT.md | Full list of 67+ blocked attack vectors | | docs/STDLIB-SECURITY.md | Standard library security analysis | | docs/LOOP-TRANSFORMATION.md | Future loop transformation design |
Roadmap
- [x] Core validator architecture
- [x] Built-in security rules
- [x] Argument validation
- [x] Unreachable code detection
- [x] Comprehensive test suite (613 tests)
- [x] CVE protection (vm2, isolated-vm, node-vm)
- [ ] Loop transformation (design complete)
- [ ] CLI tool
- [ ] VS Code extension
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
License
Apache-2.0
Credits
Built with:
- acorn - JavaScript parser
- acorn-walk - AST walker
- Part of the FrontMCP ecosystem
