@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.
Files changed (71) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +86 -0
  3. package/package.json +60 -0
  4. package/src/constants.ts +77 -0
  5. package/src/engine/RuleExecutor.ts +256 -0
  6. package/src/engine/Runner.ts +312 -0
  7. package/src/engine/SandboxedExecutor.ts +208 -0
  8. package/src/errors.ts +88 -0
  9. package/src/helpers/arista/helpers.ts +1220 -0
  10. package/src/helpers/arista/index.ts +12 -0
  11. package/src/helpers/aruba/helpers.ts +637 -0
  12. package/src/helpers/aruba/index.ts +13 -0
  13. package/src/helpers/cisco/helpers.ts +534 -0
  14. package/src/helpers/cisco/index.ts +11 -0
  15. package/src/helpers/common/helpers.ts +265 -0
  16. package/src/helpers/common/index.ts +5 -0
  17. package/src/helpers/common/validation.ts +280 -0
  18. package/src/helpers/cumulus/helpers.ts +676 -0
  19. package/src/helpers/cumulus/index.ts +12 -0
  20. package/src/helpers/extreme/helpers.ts +422 -0
  21. package/src/helpers/extreme/index.ts +12 -0
  22. package/src/helpers/fortinet/helpers.ts +892 -0
  23. package/src/helpers/fortinet/index.ts +12 -0
  24. package/src/helpers/huawei/helpers.ts +790 -0
  25. package/src/helpers/huawei/index.ts +11 -0
  26. package/src/helpers/index.ts +53 -0
  27. package/src/helpers/juniper/helpers.ts +756 -0
  28. package/src/helpers/juniper/index.ts +12 -0
  29. package/src/helpers/mikrotik/helpers.ts +722 -0
  30. package/src/helpers/mikrotik/index.ts +12 -0
  31. package/src/helpers/nokia/helpers.ts +856 -0
  32. package/src/helpers/nokia/index.ts +11 -0
  33. package/src/helpers/paloalto/helpers.ts +939 -0
  34. package/src/helpers/paloalto/index.ts +12 -0
  35. package/src/helpers/vyos/helpers.ts +429 -0
  36. package/src/helpers/vyos/index.ts +12 -0
  37. package/src/index.ts +30 -0
  38. package/src/json-rules/ExpressionEvaluator.ts +292 -0
  39. package/src/json-rules/HelperRegistry.ts +177 -0
  40. package/src/json-rules/JsonRuleCompiler.ts +339 -0
  41. package/src/json-rules/JsonRuleValidator.ts +371 -0
  42. package/src/json-rules/index.ts +97 -0
  43. package/src/json-rules/schema.json +350 -0
  44. package/src/json-rules/types.ts +303 -0
  45. package/src/pack-loader/PackLoader.ts +332 -0
  46. package/src/pack-loader/index.ts +17 -0
  47. package/src/pack-loader/types.ts +135 -0
  48. package/src/parser/IncrementalParser.ts +527 -0
  49. package/src/parser/Sanitizer.ts +104 -0
  50. package/src/parser/SchemaAwareParser.ts +504 -0
  51. package/src/parser/VendorSchema.ts +72 -0
  52. package/src/parser/vendors/arista-eos.ts +206 -0
  53. package/src/parser/vendors/aruba-aoscx.ts +123 -0
  54. package/src/parser/vendors/aruba-aosswitch.ts +113 -0
  55. package/src/parser/vendors/aruba-wlc.ts +173 -0
  56. package/src/parser/vendors/cisco-ios.ts +110 -0
  57. package/src/parser/vendors/cisco-nxos.ts +107 -0
  58. package/src/parser/vendors/cumulus-linux.ts +161 -0
  59. package/src/parser/vendors/extreme-exos.ts +154 -0
  60. package/src/parser/vendors/extreme-voss.ts +167 -0
  61. package/src/parser/vendors/fortinet-fortigate.ts +217 -0
  62. package/src/parser/vendors/huawei-vrp.ts +192 -0
  63. package/src/parser/vendors/index.ts +1521 -0
  64. package/src/parser/vendors/juniper-junos.ts +230 -0
  65. package/src/parser/vendors/mikrotik-routeros.ts +274 -0
  66. package/src/parser/vendors/nokia-sros.ts +251 -0
  67. package/src/parser/vendors/paloalto-panos.ts +264 -0
  68. package/src/parser/vendors/vyos-vyos.ts +454 -0
  69. package/src/types/ConfigNode.ts +72 -0
  70. package/src/types/DeclarativeRule.ts +158 -0
  71. package/src/types/IRule.ts +270 -0
@@ -0,0 +1,312 @@
1
+ // packages/core/src/engine/Runner.ts
2
+
3
+ import type { ConfigNode } from '../types/ConfigNode';
4
+ import type { IRule, RuleResult, Context } from '../types/IRule';
5
+ import { RuleExecutor } from './RuleExecutor';
6
+ import type { ExecutionOptions } from './RuleExecutor';
7
+
8
+ /**
9
+ * Index structure for fast rule lookup.
10
+ * Reduces selector matching from O(N×R) to O(N×k) where k << R.
11
+ */
12
+ interface RuleIndex {
13
+ /** Rules indexed by first keyword of selector (lowercase) */
14
+ byPrefix: Map<string, IRule[]>;
15
+ /** Rules with no selector (global rules that run on all nodes) */
16
+ global: IRule[];
17
+ /** Rules indexed by exact selector match (lowercase) */
18
+ exact: Map<string, IRule[]>;
19
+ }
20
+
21
+ /**
22
+ * Options for RuleEngine.
23
+ */
24
+ export interface EngineOptions {
25
+ /** Enable timeout protection for rule execution */
26
+ enableTimeoutProtection?: boolean;
27
+ /** Options for the rule executor (timeout settings) */
28
+ executionOptions?: ExecutionOptions;
29
+ }
30
+
31
+ /**
32
+ * Rule Engine with selector indexing for high-performance rule evaluation.
33
+ *
34
+ * Performance characteristics:
35
+ * - Index build: O(R) where R = number of rules
36
+ * - Per-node lookup: O(1) average case (hash map lookup)
37
+ * - Total scan: O(N×k) where N = nodes, k = average matching rules per node
38
+ *
39
+ * For 500 rules with good selector distribution, k is typically 5-20,
40
+ * giving ~25-100× improvement over naive O(N×R) scanning.
41
+ */
42
+ export class RuleEngine {
43
+ private index: RuleIndex | null = null;
44
+ private indexedRules: IRule[] = [];
45
+ private indexVersion = 0;
46
+ private executor: RuleExecutor | null = null;
47
+ private options: EngineOptions;
48
+
49
+ constructor(options: EngineOptions = {}) {
50
+ this.options = {
51
+ enableTimeoutProtection: options.enableTimeoutProtection ?? false,
52
+ executionOptions: options.executionOptions,
53
+ };
54
+
55
+ if (this.options.enableTimeoutProtection) {
56
+ this.executor = new RuleExecutor(this.options.executionOptions);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Build an index for fast rule lookup.
62
+ * Call this once when rules change, not on every scan.
63
+ *
64
+ * @param rules The rules to index
65
+ */
66
+ public buildIndex(rules: IRule[]): void {
67
+ this.index = {
68
+ byPrefix: new Map(),
69
+ global: [],
70
+ exact: new Map(),
71
+ };
72
+ this.indexedRules = rules;
73
+ this.indexVersion++;
74
+
75
+ for (const rule of rules) {
76
+ if (!rule.selector) {
77
+ // No selector = global rule, runs on everything
78
+ this.index.global.push(rule);
79
+ continue;
80
+ }
81
+
82
+ const selector = rule.selector.toLowerCase();
83
+
84
+ // Index by exact selector
85
+ const exactBucket = this.index.exact.get(selector);
86
+ if (exactBucket) {
87
+ exactBucket.push(rule);
88
+ } else {
89
+ this.index.exact.set(selector, [rule]);
90
+ }
91
+
92
+ // Index by first word (prefix) for partial matches
93
+ const prefix = selector.split(/\s+/)[0] ?? selector;
94
+ const prefixBucket = this.index.byPrefix.get(prefix);
95
+ if (prefixBucket) {
96
+ prefixBucket.push(rule);
97
+ } else {
98
+ this.index.byPrefix.set(prefix, [rule]);
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get rules that might match a node.
105
+ * Returns a small subset of total rules based on prefix matching.
106
+ *
107
+ * @param node The configuration node to find candidate rules for
108
+ * @returns Array of rules that may match the node
109
+ */
110
+ private getCandidateRules(node: ConfigNode): IRule[] {
111
+ if (!this.index) {
112
+ return this.indexedRules; // Fallback to all rules if no index
113
+ }
114
+
115
+ const nodeId = node.id.toLowerCase();
116
+ const nodePrefix = nodeId.split(/\s+/)[0] ?? nodeId;
117
+
118
+ // Start with global rules (always run)
119
+ const candidates: IRule[] = [...this.index.global];
120
+
121
+ // Add rules matching the node's prefix
122
+ const prefixRules = this.index.byPrefix.get(nodePrefix);
123
+ if (prefixRules) {
124
+ candidates.push(...prefixRules);
125
+ }
126
+
127
+ return candidates;
128
+ }
129
+
130
+ /**
131
+ * Run rules against nodes using the index for fast lookup.
132
+ *
133
+ * @param nodes The root nodes of the configuration AST
134
+ * @param rules Optional rules array - if provided and different from indexed rules, rebuilds index
135
+ * @param context Optional global context to pass to rules
136
+ * @returns Array of RuleResult objects
137
+ */
138
+ public run(
139
+ nodes: ConfigNode[],
140
+ rules?: IRule[],
141
+ context: Partial<Context> = {}
142
+ ): RuleResult[] {
143
+ // Rebuild index if rules provided and different
144
+ if (rules && rules !== this.indexedRules) {
145
+ this.buildIndex(rules);
146
+ }
147
+
148
+ // If no rules indexed, return empty results
149
+ if (this.indexedRules.length === 0) {
150
+ return [];
151
+ }
152
+
153
+ const results: RuleResult[] = [];
154
+
155
+ // Create context once with lazy AST getter
156
+ const ruleContext: Context = {
157
+ ...context,
158
+ getAst: () => nodes,
159
+ };
160
+
161
+ const visit = (node: ConfigNode): void => {
162
+ // Only check candidate rules, not all rules
163
+ const candidates = this.getCandidateRules(node);
164
+
165
+ for (const rule of candidates) {
166
+ if (this.matchesSelector(node, rule.selector)) {
167
+ // Use executor if timeout protection is enabled
168
+ if (this.executor) {
169
+ const result = this.executor.execute(rule, node, ruleContext);
170
+ if (result) {
171
+ results.push(result);
172
+ }
173
+ } else {
174
+ // Direct execution without timeout protection
175
+ try {
176
+ const result = rule.check(node, ruleContext);
177
+ if (result) {
178
+ results.push(result);
179
+ }
180
+ } catch (error) {
181
+ results.push({
182
+ passed: false,
183
+ message: `Rule execution error: ${
184
+ error instanceof Error ? error.message : String(error)
185
+ }`,
186
+ ruleId: rule.id,
187
+ nodeId: node.id,
188
+ level: 'error',
189
+ loc: node.loc,
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ // Recurse into children
197
+ for (const child of node.children) {
198
+ visit(child);
199
+ }
200
+ };
201
+
202
+ for (const node of nodes) {
203
+ visit(node);
204
+ }
205
+
206
+ return results;
207
+ }
208
+
209
+ /**
210
+ * Checks if a node matches a rule's selector.
211
+ * Uses case-insensitive prefix matching.
212
+ *
213
+ * @param node The configuration node
214
+ * @param selector The selector string (e.g., "interface", "router bgp")
215
+ * @returns True if the node matches the selector
216
+ */
217
+ private matchesSelector(node: ConfigNode, selector?: string): boolean {
218
+ if (!selector) return true;
219
+ return node.id.toLowerCase().startsWith(selector.toLowerCase());
220
+ }
221
+
222
+ /**
223
+ * Check if the index needs rebuilding.
224
+ * Useful for determining when to call buildIndex().
225
+ *
226
+ * @param rules The rules to check against
227
+ * @returns True if the rules differ from the indexed rules
228
+ */
229
+ public needsReindex(rules: IRule[]): boolean {
230
+ return rules !== this.indexedRules;
231
+ }
232
+
233
+ /**
234
+ * Get the current index version.
235
+ * Increments each time buildIndex() is called.
236
+ */
237
+ public getIndexVersion(): number {
238
+ return this.indexVersion;
239
+ }
240
+
241
+ /**
242
+ * Get statistics about the current index.
243
+ * Useful for debugging and performance monitoring.
244
+ */
245
+ public getIndexStats(): {
246
+ totalRules: number;
247
+ globalRules: number;
248
+ prefixBuckets: number;
249
+ exactBuckets: number;
250
+ avgRulesPerPrefix: number;
251
+ } {
252
+ if (!this.index) {
253
+ return {
254
+ totalRules: 0,
255
+ globalRules: 0,
256
+ prefixBuckets: 0,
257
+ exactBuckets: 0,
258
+ avgRulesPerPrefix: 0,
259
+ };
260
+ }
261
+
262
+ const prefixBuckets = this.index.byPrefix.size;
263
+ let totalPrefixRules = 0;
264
+ for (const bucket of this.index.byPrefix.values()) {
265
+ totalPrefixRules += bucket.length;
266
+ }
267
+
268
+ return {
269
+ totalRules: this.indexedRules.length,
270
+ globalRules: this.index.global.length,
271
+ prefixBuckets,
272
+ exactBuckets: this.index.exact.size,
273
+ avgRulesPerPrefix: prefixBuckets > 0 ? totalPrefixRules / prefixBuckets : 0,
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Clear the index and release memory.
279
+ */
280
+ public clearIndex(): void {
281
+ this.index = null;
282
+ this.indexedRules = [];
283
+ }
284
+
285
+ /**
286
+ * Get the rule executor (if timeout protection is enabled).
287
+ */
288
+ public getExecutor(): RuleExecutor | null {
289
+ return this.executor;
290
+ }
291
+
292
+ /**
293
+ * Get list of auto-disabled rules (if timeout protection is enabled).
294
+ */
295
+ public getDisabledRules(): string[] {
296
+ return this.executor?.getDisabledRules() ?? [];
297
+ }
298
+
299
+ /**
300
+ * Re-enable a previously disabled rule.
301
+ */
302
+ public enableRule(ruleId: string): void {
303
+ this.executor?.enableRule(ruleId);
304
+ }
305
+
306
+ /**
307
+ * Reset executor state (timeout counts, disabled rules).
308
+ */
309
+ public resetExecutor(): void {
310
+ this.executor?.resetAll();
311
+ }
312
+ }
@@ -0,0 +1,208 @@
1
+ // packages/core/src/engine/SandboxedExecutor.ts
2
+
3
+ /**
4
+ * SEC-001: Sandboxed Executor for Declarative Rules
5
+ *
6
+ * Provides safe evaluation of declarative rules without executing
7
+ * arbitrary JavaScript. Custom code blocks run in a VM sandbox
8
+ * with strict timeouts and limited API access.
9
+ */
10
+
11
+ import { createContext, Script, type Context as VMContext } from 'vm';
12
+ import type { ConfigNode } from '../types/ConfigNode';
13
+ import type { Context } from '../types/IRule';
14
+ import type { IRule, RuleResult, RuleVendor } from '../types/IRule';
15
+ import type { DeclarativeRule, DeclarativeCheck } from '../types/DeclarativeRule';
16
+
17
+ /** Timeout for custom code execution in milliseconds */
18
+ const CUSTOM_CODE_TIMEOUT_MS = 100;
19
+
20
+ /**
21
+ * Compiles a DeclarativeRule into an IRule with a safe check function.
22
+ *
23
+ * For most declarative checks, this produces a native function that
24
+ * executes without any sandboxing overhead. Only 'custom' type checks
25
+ * require VM sandboxing.
26
+ */
27
+ export class SandboxedExecutor {
28
+ private readonly sandbox: Record<string, unknown>;
29
+ private readonly vmContext: VMContext;
30
+
31
+ constructor() {
32
+ this.sandbox = this.createSandbox();
33
+ this.vmContext = createContext(this.sandbox);
34
+ }
35
+
36
+ /**
37
+ * Compiles a declarative rule into an executable IRule.
38
+ */
39
+ compileRule(decl: DeclarativeRule): IRule {
40
+ return {
41
+ id: decl.id,
42
+ selector: decl.selector,
43
+ vendor: decl.vendor as RuleVendor | RuleVendor[] | undefined,
44
+ metadata: decl.metadata,
45
+ check: (node: ConfigNode, ctx: Context): RuleResult => {
46
+ return this.evaluate(decl.check, node, ctx, decl);
47
+ },
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Evaluates a declarative check condition against a node.
53
+ */
54
+ private evaluate(
55
+ check: DeclarativeCheck,
56
+ node: ConfigNode,
57
+ ctx: Context,
58
+ rule: DeclarativeRule
59
+ ): RuleResult {
60
+ const passed = this.evaluateCondition(check, node);
61
+ return {
62
+ passed,
63
+ message: passed
64
+ ? `${rule.id}: Check passed`
65
+ : `${rule.id}: Check failed - ${rule.metadata.remediation ?? 'See rule documentation'}`,
66
+ ruleId: rule.id,
67
+ nodeId: node.id,
68
+ level: rule.metadata.level,
69
+ loc: node.loc,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Recursively evaluates a declarative check condition.
75
+ * Returns true if the condition is satisfied.
76
+ */
77
+ private evaluateCondition(check: DeclarativeCheck, node: ConfigNode): boolean {
78
+ switch (check.type) {
79
+ case 'match':
80
+ return new RegExp(check.pattern, check.flags).test(node.id);
81
+
82
+ case 'not_match':
83
+ return !new RegExp(check.pattern, check.flags).test(node.id);
84
+
85
+ case 'contains':
86
+ return node.id.includes(check.text);
87
+
88
+ case 'not_contains':
89
+ return !node.id.includes(check.text);
90
+
91
+ case 'child_exists':
92
+ return node.children.some(c =>
93
+ c.id.toLowerCase().startsWith(check.selector.toLowerCase())
94
+ );
95
+
96
+ case 'child_not_exists':
97
+ return !node.children.some(c =>
98
+ c.id.toLowerCase().startsWith(check.selector.toLowerCase())
99
+ );
100
+
101
+ case 'child_matches': {
102
+ const matchingChildren = node.children.filter(c =>
103
+ c.id.toLowerCase().startsWith(check.selector.toLowerCase())
104
+ );
105
+ const regex = new RegExp(check.pattern, check.flags);
106
+ return matchingChildren.some(c => regex.test(c.id));
107
+ }
108
+
109
+ case 'child_contains': {
110
+ const containsChildren = node.children.filter(c =>
111
+ c.id.toLowerCase().startsWith(check.selector.toLowerCase())
112
+ );
113
+ return containsChildren.some(c => c.id.includes(check.text));
114
+ }
115
+
116
+ case 'and':
117
+ return check.conditions.every(c => this.evaluateCondition(c, node));
118
+
119
+ case 'or':
120
+ return check.conditions.some(c => this.evaluateCondition(c, node));
121
+
122
+ case 'not':
123
+ return !this.evaluateCondition(check.condition, node);
124
+
125
+ case 'custom':
126
+ return this.executeCustomCode(check.code, node);
127
+
128
+ default:
129
+ // Unknown check type - fail closed for safety
130
+ return false;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Executes custom code in a VM sandbox.
136
+ * Returns the boolean result of the code execution.
137
+ */
138
+ private executeCustomCode(code: string, node: ConfigNode): boolean {
139
+ // Prepare a frozen copy of the node for the sandbox
140
+ this.sandbox.node = Object.freeze({
141
+ id: node.id,
142
+ type: node.type,
143
+ children: node.children.map(c => Object.freeze({
144
+ id: c.id,
145
+ type: c.type,
146
+ })),
147
+ });
148
+
149
+ try {
150
+ const wrappedCode = `(function() { ${code} })()`;
151
+ const script = new Script(wrappedCode, {
152
+ filename: 'custom-check.js',
153
+ timeout: CUSTOM_CODE_TIMEOUT_MS,
154
+ } as { filename: string; timeout: number });
155
+ const result = script.runInContext(this.vmContext);
156
+ return Boolean(result);
157
+ } catch {
158
+ // Any error (timeout, syntax, runtime) = fail closed
159
+ return false;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Creates a minimal sandbox for custom code execution.
165
+ * Only provides safe, read-only access to basic JavaScript features.
166
+ */
167
+ private createSandbox(): Record<string, unknown> {
168
+ return Object.freeze({
169
+ // The node being checked (set dynamically)
170
+ node: null,
171
+
172
+ // Safe built-ins (frozen)
173
+ Boolean,
174
+ Number,
175
+ String,
176
+ Array,
177
+ Object,
178
+ RegExp,
179
+
180
+ // Safe JSON access
181
+ JSON: Object.freeze({
182
+ parse: JSON.parse,
183
+ stringify: JSON.stringify,
184
+ }),
185
+
186
+ // Safe Math access
187
+ Math: Object.freeze(Math),
188
+
189
+ // Primitives
190
+ true: true,
191
+ false: false,
192
+ undefined,
193
+ null: null,
194
+ NaN,
195
+ Infinity,
196
+
197
+ // No console, no require, no import, no process, no global
198
+ });
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Creates a SandboxedExecutor instance.
204
+ * Use this for compiling declarative rules.
205
+ */
206
+ export function createSandboxedExecutor(): SandboxedExecutor {
207
+ return new SandboxedExecutor();
208
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,88 @@
1
+ // packages/core/src/errors.ts
2
+
3
+ /**
4
+ * Base error class for all SentriFlow errors.
5
+ * Provides structured error handling with error codes.
6
+ */
7
+ export class SentriflowError extends Error {
8
+ constructor(
9
+ public readonly code: string,
10
+ message: string,
11
+ public readonly details?: Record<string, unknown>
12
+ ) {
13
+ super(message);
14
+ this.name = 'SentriflowError';
15
+ // Maintains proper stack trace for where error was thrown (V8 only)
16
+ if (Error.captureStackTrace) {
17
+ Error.captureStackTrace(this, this.constructor);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Returns a user-friendly error message without internal details.
23
+ */
24
+ toUserMessage(): string {
25
+ return `[${this.code}] ${this.message}`;
26
+ }
27
+
28
+ /**
29
+ * Returns a JSON representation for logging.
30
+ */
31
+ toJSON(): Record<string, unknown> {
32
+ return {
33
+ code: this.code,
34
+ message: this.message,
35
+ details: this.details,
36
+ };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Error thrown when configuration loading fails.
42
+ */
43
+ export class SentriflowConfigError extends SentriflowError {
44
+ constructor(message: string, details?: Record<string, unknown>) {
45
+ super('CONFIG_ERROR', message, details);
46
+ this.name = 'SentriflowConfigError';
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Error thrown when path validation fails.
52
+ */
53
+ export class SentriflowPathError extends SentriflowError {
54
+ constructor(message: string, details?: Record<string, unknown>) {
55
+ super('PATH_ERROR', message, details);
56
+ this.name = 'SentriflowPathError';
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Error thrown when parsing fails.
62
+ */
63
+ export class SentriflowParseError extends SentriflowError {
64
+ constructor(message: string, line?: number) {
65
+ super('PARSE_ERROR', message, line !== undefined ? { line } : undefined);
66
+ this.name = 'SentriflowParseError';
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Error thrown when rule validation or execution fails.
72
+ */
73
+ export class SentriflowRuleError extends SentriflowError {
74
+ constructor(message: string, ruleId?: string) {
75
+ super('RULE_ERROR', message, ruleId ? { ruleId } : undefined);
76
+ this.name = 'SentriflowRuleError';
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Error thrown when input size exceeds limits.
82
+ */
83
+ export class SentriflowSizeLimitError extends SentriflowError {
84
+ constructor(message: string, details?: Record<string, unknown>) {
85
+ super('SIZE_LIMIT_ERROR', message, details);
86
+ this.name = 'SentriflowSizeLimitError';
87
+ }
88
+ }