@sentriflow/core 0.1.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/LICENSE +190 -0
- package/README.md +86 -0
- package/package.json +60 -0
- package/src/constants.ts +77 -0
- package/src/engine/RuleExecutor.ts +256 -0
- package/src/engine/Runner.ts +312 -0
- package/src/engine/SandboxedExecutor.ts +208 -0
- package/src/errors.ts +88 -0
- package/src/helpers/arista/helpers.ts +1220 -0
- package/src/helpers/arista/index.ts +12 -0
- package/src/helpers/aruba/helpers.ts +637 -0
- package/src/helpers/aruba/index.ts +13 -0
- package/src/helpers/cisco/helpers.ts +534 -0
- package/src/helpers/cisco/index.ts +11 -0
- package/src/helpers/common/helpers.ts +265 -0
- package/src/helpers/common/index.ts +5 -0
- package/src/helpers/common/validation.ts +280 -0
- package/src/helpers/cumulus/helpers.ts +676 -0
- package/src/helpers/cumulus/index.ts +12 -0
- package/src/helpers/extreme/helpers.ts +422 -0
- package/src/helpers/extreme/index.ts +12 -0
- package/src/helpers/fortinet/helpers.ts +892 -0
- package/src/helpers/fortinet/index.ts +12 -0
- package/src/helpers/huawei/helpers.ts +790 -0
- package/src/helpers/huawei/index.ts +11 -0
- package/src/helpers/index.ts +53 -0
- package/src/helpers/juniper/helpers.ts +756 -0
- package/src/helpers/juniper/index.ts +12 -0
- package/src/helpers/mikrotik/helpers.ts +722 -0
- package/src/helpers/mikrotik/index.ts +12 -0
- package/src/helpers/nokia/helpers.ts +856 -0
- package/src/helpers/nokia/index.ts +11 -0
- package/src/helpers/paloalto/helpers.ts +939 -0
- package/src/helpers/paloalto/index.ts +12 -0
- package/src/helpers/vyos/helpers.ts +429 -0
- package/src/helpers/vyos/index.ts +12 -0
- package/src/index.ts +30 -0
- package/src/json-rules/ExpressionEvaluator.ts +292 -0
- package/src/json-rules/HelperRegistry.ts +177 -0
- package/src/json-rules/JsonRuleCompiler.ts +339 -0
- package/src/json-rules/JsonRuleValidator.ts +371 -0
- package/src/json-rules/index.ts +97 -0
- package/src/json-rules/schema.json +350 -0
- package/src/json-rules/types.ts +303 -0
- package/src/pack-loader/PackLoader.ts +332 -0
- package/src/pack-loader/index.ts +17 -0
- package/src/pack-loader/types.ts +135 -0
- package/src/parser/IncrementalParser.ts +527 -0
- package/src/parser/Sanitizer.ts +104 -0
- package/src/parser/SchemaAwareParser.ts +504 -0
- package/src/parser/VendorSchema.ts +72 -0
- package/src/parser/vendors/arista-eos.ts +206 -0
- package/src/parser/vendors/aruba-aoscx.ts +123 -0
- package/src/parser/vendors/aruba-aosswitch.ts +113 -0
- package/src/parser/vendors/aruba-wlc.ts +173 -0
- package/src/parser/vendors/cisco-ios.ts +110 -0
- package/src/parser/vendors/cisco-nxos.ts +107 -0
- package/src/parser/vendors/cumulus-linux.ts +161 -0
- package/src/parser/vendors/extreme-exos.ts +154 -0
- package/src/parser/vendors/extreme-voss.ts +167 -0
- package/src/parser/vendors/fortinet-fortigate.ts +217 -0
- package/src/parser/vendors/huawei-vrp.ts +192 -0
- package/src/parser/vendors/index.ts +1521 -0
- package/src/parser/vendors/juniper-junos.ts +230 -0
- package/src/parser/vendors/mikrotik-routeros.ts +274 -0
- package/src/parser/vendors/nokia-sros.ts +251 -0
- package/src/parser/vendors/paloalto-panos.ts +264 -0
- package/src/parser/vendors/vyos-vyos.ts +454 -0
- package/src/types/ConfigNode.ts +72 -0
- package/src/types/DeclarativeRule.ts +158 -0
- package/src/types/IRule.ts +270 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// packages/core/src/json-rules/ExpressionEvaluator.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sandboxed Expression Evaluator for JSON Rules
|
|
5
|
+
*
|
|
6
|
+
* Evaluates simple JavaScript expressions in a secure sandbox.
|
|
7
|
+
* Provides access to helper functions while blocking dangerous operations.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createContext, Script, type Context as VMContext } from 'vm';
|
|
11
|
+
import type { ConfigNode } from '../types/ConfigNode';
|
|
12
|
+
import { type HelperRegistry, getHelperRegistry, VENDOR_NAMESPACES } from './HelperRegistry';
|
|
13
|
+
|
|
14
|
+
/** Maximum expression length to prevent DoS */
|
|
15
|
+
const MAX_EXPR_LENGTH = 1000;
|
|
16
|
+
|
|
17
|
+
/** Timeout for expression evaluation in milliseconds */
|
|
18
|
+
const EXPR_TIMEOUT_MS = 50;
|
|
19
|
+
|
|
20
|
+
/** Patterns that are blocked for security */
|
|
21
|
+
const BLOCKED_PATTERNS = [
|
|
22
|
+
'require',
|
|
23
|
+
'import',
|
|
24
|
+
'eval',
|
|
25
|
+
'Function',
|
|
26
|
+
'process',
|
|
27
|
+
'global',
|
|
28
|
+
'globalThis',
|
|
29
|
+
'window',
|
|
30
|
+
'__proto__',
|
|
31
|
+
'constructor',
|
|
32
|
+
'prototype',
|
|
33
|
+
'Reflect',
|
|
34
|
+
'Proxy',
|
|
35
|
+
'module',
|
|
36
|
+
'exports',
|
|
37
|
+
'Buffer',
|
|
38
|
+
'setTimeout',
|
|
39
|
+
'setInterval',
|
|
40
|
+
'setImmediate',
|
|
41
|
+
'clearTimeout',
|
|
42
|
+
'clearInterval',
|
|
43
|
+
'clearImmediate',
|
|
44
|
+
'fetch',
|
|
45
|
+
'XMLHttpRequest',
|
|
46
|
+
'WebSocket',
|
|
47
|
+
// Additional security patterns
|
|
48
|
+
'arguments', // Special object in functions
|
|
49
|
+
'this', // Context access
|
|
50
|
+
'with', // Scope manipulation
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pre-compiled script cache for performance.
|
|
55
|
+
*/
|
|
56
|
+
const scriptCache = new Map<string, Script>();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Freeze an object deeply to prevent modification.
|
|
60
|
+
*/
|
|
61
|
+
function deepFreeze<T>(obj: T): T {
|
|
62
|
+
if (obj === null || typeof obj !== 'object') {
|
|
63
|
+
return obj;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Object.freeze(obj);
|
|
67
|
+
|
|
68
|
+
for (const key of Object.keys(obj)) {
|
|
69
|
+
const value = (obj as Record<string, unknown>)[key];
|
|
70
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
71
|
+
deepFreeze(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return obj;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a frozen, safe copy of a ConfigNode for sandbox use.
|
|
80
|
+
* Only exposes safe properties, no methods or circular references.
|
|
81
|
+
*/
|
|
82
|
+
function createSafeNode(node: ConfigNode): Readonly<{
|
|
83
|
+
id: string;
|
|
84
|
+
type: string;
|
|
85
|
+
rawText: string;
|
|
86
|
+
params: readonly string[];
|
|
87
|
+
children: readonly ReturnType<typeof createSafeNode>[];
|
|
88
|
+
}> {
|
|
89
|
+
return deepFreeze({
|
|
90
|
+
id: node.id,
|
|
91
|
+
type: node.type,
|
|
92
|
+
rawText: node.rawText,
|
|
93
|
+
params: [...node.params],
|
|
94
|
+
children: node.children.map(createSafeNode),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate an expression for security.
|
|
100
|
+
* Returns true if the expression is safe to evaluate.
|
|
101
|
+
*/
|
|
102
|
+
export function isValidExpression(expr: string): boolean {
|
|
103
|
+
// Check length
|
|
104
|
+
if (expr.length > MAX_EXPR_LENGTH) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for blocked patterns
|
|
109
|
+
const exprLower = expr.toLowerCase();
|
|
110
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
111
|
+
if (exprLower.includes(pattern.toLowerCase())) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Block template literals with expressions
|
|
117
|
+
if (expr.includes('${')) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Block assignment operators
|
|
122
|
+
if (/[^=!<>]=[^=]/.test(expr)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Expression Evaluator class for sandboxed expression evaluation.
|
|
131
|
+
* Provides pre-compilation and caching for performance.
|
|
132
|
+
*/
|
|
133
|
+
export class ExpressionEvaluator {
|
|
134
|
+
private readonly sandbox: VMContext;
|
|
135
|
+
private readonly registry: HelperRegistry;
|
|
136
|
+
|
|
137
|
+
constructor(registry?: HelperRegistry) {
|
|
138
|
+
this.registry = registry ?? getHelperRegistry();
|
|
139
|
+
this.sandbox = this.createSandbox();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a sandboxed VM context with helpers available.
|
|
144
|
+
*/
|
|
145
|
+
private createSandbox(): VMContext {
|
|
146
|
+
// Start with safe primitives
|
|
147
|
+
const sandboxObj: Record<string, unknown> = {
|
|
148
|
+
// Safe primitives
|
|
149
|
+
true: true,
|
|
150
|
+
false: false,
|
|
151
|
+
undefined: undefined,
|
|
152
|
+
null: null,
|
|
153
|
+
NaN: NaN,
|
|
154
|
+
Infinity: Infinity,
|
|
155
|
+
|
|
156
|
+
// Safe built-in constructors (frozen)
|
|
157
|
+
Boolean: Object.freeze(Boolean),
|
|
158
|
+
Number: Object.freeze(Number),
|
|
159
|
+
String: Object.freeze(String),
|
|
160
|
+
Array: Object.freeze(Array),
|
|
161
|
+
Object: Object.freeze(Object),
|
|
162
|
+
RegExp: Object.freeze(RegExp),
|
|
163
|
+
JSON: Object.freeze(JSON),
|
|
164
|
+
Math: Object.freeze(Math),
|
|
165
|
+
|
|
166
|
+
// Node placeholder (set per evaluation)
|
|
167
|
+
node: null,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Add common helpers at top level
|
|
171
|
+
for (const [key, value] of Object.entries(this.registry)) {
|
|
172
|
+
if (typeof value === 'function') {
|
|
173
|
+
sandboxObj[key] = value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add vendor namespaces
|
|
178
|
+
for (const namespace of VENDOR_NAMESPACES) {
|
|
179
|
+
const vendorHelpers = this.registry[namespace];
|
|
180
|
+
if (vendorHelpers && typeof vendorHelpers === 'object') {
|
|
181
|
+
sandboxObj[namespace] = Object.freeze({ ...vendorHelpers });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return createContext(Object.freeze(sandboxObj));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Pre-compile an expression for later evaluation.
|
|
190
|
+
* Call this at rule load time for performance.
|
|
191
|
+
*
|
|
192
|
+
* @param expr The expression to compile
|
|
193
|
+
* @returns true if compilation succeeded
|
|
194
|
+
*/
|
|
195
|
+
precompile(expr: string): boolean {
|
|
196
|
+
if (!isValidExpression(expr)) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (scriptCache.has(expr)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const script = new Script(`(${expr})`, {
|
|
206
|
+
filename: 'expr.js',
|
|
207
|
+
});
|
|
208
|
+
scriptCache.set(expr, script);
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Evaluate an expression against a node.
|
|
217
|
+
*
|
|
218
|
+
* @param expr The expression to evaluate
|
|
219
|
+
* @param node The ConfigNode to evaluate against
|
|
220
|
+
* @returns The evaluation result as a boolean
|
|
221
|
+
*/
|
|
222
|
+
evaluate(expr: string, node: ConfigNode): boolean {
|
|
223
|
+
// Validate expression
|
|
224
|
+
if (!isValidExpression(expr)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Get or compile script
|
|
229
|
+
let script = scriptCache.get(expr);
|
|
230
|
+
if (!script) {
|
|
231
|
+
try {
|
|
232
|
+
script = new Script(`(${expr})`, {
|
|
233
|
+
filename: 'expr.js',
|
|
234
|
+
});
|
|
235
|
+
scriptCache.set(expr, script);
|
|
236
|
+
} catch {
|
|
237
|
+
return false; // Compilation error
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Set node in sandbox (create a new context each time for isolation)
|
|
242
|
+
const evalContext = createContext({
|
|
243
|
+
...this.sandbox,
|
|
244
|
+
node: createSafeNode(node),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const result = script.runInContext(evalContext, {
|
|
249
|
+
timeout: EXPR_TIMEOUT_MS,
|
|
250
|
+
});
|
|
251
|
+
return Boolean(result);
|
|
252
|
+
} catch {
|
|
253
|
+
return false; // Runtime error or timeout
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Clear the script cache (useful for testing).
|
|
259
|
+
*/
|
|
260
|
+
static clearCache(): void {
|
|
261
|
+
scriptCache.clear();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create a new ExpressionEvaluator instance.
|
|
267
|
+
* The evaluator can be reused across multiple rule evaluations.
|
|
268
|
+
*/
|
|
269
|
+
export function createExpressionEvaluator(registry?: HelperRegistry): ExpressionEvaluator {
|
|
270
|
+
return new ExpressionEvaluator(registry);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Default singleton evaluator for convenience
|
|
274
|
+
let defaultEvaluator: ExpressionEvaluator | null = null;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get the default expression evaluator (singleton).
|
|
278
|
+
*/
|
|
279
|
+
export function getExpressionEvaluator(): ExpressionEvaluator {
|
|
280
|
+
if (!defaultEvaluator) {
|
|
281
|
+
defaultEvaluator = new ExpressionEvaluator();
|
|
282
|
+
}
|
|
283
|
+
return defaultEvaluator;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Clear the default evaluator (useful for testing).
|
|
288
|
+
*/
|
|
289
|
+
export function clearExpressionEvaluator(): void {
|
|
290
|
+
defaultEvaluator = null;
|
|
291
|
+
ExpressionEvaluator.clearCache();
|
|
292
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// packages/core/src/json-rules/HelperRegistry.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper Registry for JSON Rules
|
|
5
|
+
*
|
|
6
|
+
* Provides a registry of all helper functions available to JSON rules.
|
|
7
|
+
* Helpers are organized by namespace (vendor) for clarity.
|
|
8
|
+
* Vendor namespaces are derived dynamically from the helpers module.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as helpers from '../helpers';
|
|
12
|
+
import { VENDOR_NAMESPACES as HELPER_VENDOR_NAMESPACES, getAllVendorModules } from '../helpers';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type representing any helper function.
|
|
16
|
+
*/
|
|
17
|
+
export type HelperFunction = (...args: unknown[]) => unknown;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Vendor namespace containing helper functions.
|
|
21
|
+
*/
|
|
22
|
+
export type VendorHelpers = Record<string, HelperFunction>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Vendor namespaces for helper organization.
|
|
26
|
+
* Dynamically derived from the helpers module - single source of truth.
|
|
27
|
+
*/
|
|
28
|
+
export const VENDOR_NAMESPACES = HELPER_VENDOR_NAMESPACES;
|
|
29
|
+
|
|
30
|
+
export type VendorNamespace = (typeof VENDOR_NAMESPACES)[number];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Complete helper registry with common helpers and vendor namespaces.
|
|
34
|
+
* Vendor properties are dynamically typed based on VendorNamespace.
|
|
35
|
+
*/
|
|
36
|
+
export type HelperRegistry = {
|
|
37
|
+
// Common helpers (no namespace required)
|
|
38
|
+
[key: string]: HelperFunction | VendorHelpers;
|
|
39
|
+
} & {
|
|
40
|
+
// Vendor namespaces - dynamically typed from VendorNamespace
|
|
41
|
+
[K in VendorNamespace]: VendorHelpers;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract only function exports from a module object.
|
|
46
|
+
*/
|
|
47
|
+
function extractFunctions(module: Record<string, unknown>): VendorHelpers {
|
|
48
|
+
const result: VendorHelpers = {};
|
|
49
|
+
for (const [key, value] of Object.entries(module)) {
|
|
50
|
+
if (typeof value === 'function') {
|
|
51
|
+
result[key] = value as HelperFunction;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a helper registry with all available helpers.
|
|
59
|
+
* Common helpers are available at the top level.
|
|
60
|
+
* Vendor-specific helpers are namespaced (e.g., "cisco.isTrunkPort").
|
|
61
|
+
*/
|
|
62
|
+
export function createHelperRegistry(): HelperRegistry {
|
|
63
|
+
// Extract common helpers (these are re-exported at the top level)
|
|
64
|
+
const commonHelpers = extractFunctions(helpers as unknown as Record<string, unknown>);
|
|
65
|
+
|
|
66
|
+
// Get all vendor modules dynamically
|
|
67
|
+
const vendorModules = getAllVendorModules();
|
|
68
|
+
|
|
69
|
+
// Build registry with common helpers and vendor namespaces
|
|
70
|
+
const registry = { ...commonHelpers } as HelperRegistry;
|
|
71
|
+
|
|
72
|
+
// Add each vendor namespace dynamically
|
|
73
|
+
for (const namespace of VENDOR_NAMESPACES) {
|
|
74
|
+
const vendorModule = vendorModules[namespace];
|
|
75
|
+
if (vendorModule) {
|
|
76
|
+
registry[namespace] = extractFunctions(vendorModule as unknown as Record<string, unknown>);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return registry;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a helper function by name.
|
|
85
|
+
* Supports both flat names (e.g., "hasChildCommand") and
|
|
86
|
+
* namespaced names (e.g., "cisco.isTrunkPort").
|
|
87
|
+
*
|
|
88
|
+
* @param registry The helper registry
|
|
89
|
+
* @param helperName The helper name to resolve
|
|
90
|
+
* @returns The helper function, or undefined if not found
|
|
91
|
+
*/
|
|
92
|
+
export function resolveHelper(
|
|
93
|
+
registry: HelperRegistry,
|
|
94
|
+
helperName: string
|
|
95
|
+
): HelperFunction | undefined {
|
|
96
|
+
// Check for namespaced helper (e.g., "cisco.isTrunkPort")
|
|
97
|
+
if (helperName.includes('.')) {
|
|
98
|
+
const [namespace, name] = helperName.split('.', 2);
|
|
99
|
+
if (!namespace || !name) return undefined;
|
|
100
|
+
|
|
101
|
+
const vendorHelpers = registry[namespace];
|
|
102
|
+
if (vendorHelpers && typeof vendorHelpers === 'object') {
|
|
103
|
+
const helper = vendorHelpers[name];
|
|
104
|
+
return typeof helper === 'function' ? helper : undefined;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Try common helpers at top level
|
|
110
|
+
const helper = registry[helperName];
|
|
111
|
+
if (typeof helper === 'function') {
|
|
112
|
+
return helper;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get a list of all available helper names.
|
|
120
|
+
* Useful for validation and documentation.
|
|
121
|
+
*
|
|
122
|
+
* @param registry The helper registry
|
|
123
|
+
* @returns Array of helper names (both flat and namespaced)
|
|
124
|
+
*/
|
|
125
|
+
export function getAvailableHelpers(registry: HelperRegistry): string[] {
|
|
126
|
+
const helpers: string[] = [];
|
|
127
|
+
|
|
128
|
+
// Add common helpers
|
|
129
|
+
for (const [key, value] of Object.entries(registry)) {
|
|
130
|
+
if (typeof value === 'function') {
|
|
131
|
+
helpers.push(key);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add namespaced helpers
|
|
136
|
+
for (const namespace of VENDOR_NAMESPACES) {
|
|
137
|
+
const vendorHelpers = registry[namespace];
|
|
138
|
+
if (vendorHelpers && typeof vendorHelpers === 'object') {
|
|
139
|
+
for (const key of Object.keys(vendorHelpers)) {
|
|
140
|
+
helpers.push(`${namespace}.${key}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return helpers.sort();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if a helper name exists in the registry.
|
|
150
|
+
*
|
|
151
|
+
* @param registry The helper registry
|
|
152
|
+
* @param helperName The helper name to check
|
|
153
|
+
* @returns true if the helper exists
|
|
154
|
+
*/
|
|
155
|
+
export function hasHelper(registry: HelperRegistry, helperName: string): boolean {
|
|
156
|
+
return resolveHelper(registry, helperName) !== undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Singleton registry instance for performance
|
|
160
|
+
let cachedRegistry: HelperRegistry | null = null;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the global helper registry (cached for performance).
|
|
164
|
+
*/
|
|
165
|
+
export function getHelperRegistry(): HelperRegistry {
|
|
166
|
+
if (!cachedRegistry) {
|
|
167
|
+
cachedRegistry = createHelperRegistry();
|
|
168
|
+
}
|
|
169
|
+
return cachedRegistry;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Clear the cached registry (useful for testing).
|
|
174
|
+
*/
|
|
175
|
+
export function clearHelperRegistryCache(): void {
|
|
176
|
+
cachedRegistry = null;
|
|
177
|
+
}
|