@sovr/engine 3.1.0 → 3.3.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,3 +1,654 @@
1
+ // src/expressionTree.ts
2
+ function compileFromJSON(rule) {
3
+ const ast = parseNode(rule);
4
+ const variables = extractVariables(ast);
5
+ return {
6
+ ast,
7
+ evaluate: (ctx) => evaluateNode(ast, ctx),
8
+ toString: () => nodeToString(ast),
9
+ variables
10
+ };
11
+ }
12
+ function parseNode(rule) {
13
+ if (rule === null || rule === void 0) {
14
+ return { type: "LITERAL", value: false };
15
+ }
16
+ if (rule.and && Array.isArray(rule.and)) {
17
+ return {
18
+ type: "AND",
19
+ children: rule.and.map((child) => parseNode(child))
20
+ };
21
+ }
22
+ if (rule.or && Array.isArray(rule.or)) {
23
+ return {
24
+ type: "OR",
25
+ children: rule.or.map((child) => parseNode(child))
26
+ };
27
+ }
28
+ if (rule.not) {
29
+ return {
30
+ type: "NOT",
31
+ child: parseNode(rule.not)
32
+ };
33
+ }
34
+ if (rule.fn) {
35
+ return {
36
+ type: "COMPARE",
37
+ operator: rule.op || "==",
38
+ left: {
39
+ type: "FUNCTION",
40
+ name: rule.fn,
41
+ args: (rule.args || []).map(
42
+ (a) => typeof a === "string" ? { type: "VARIABLE", path: a } : { type: "LITERAL", value: a }
43
+ )
44
+ },
45
+ right: { type: "LITERAL", value: rule.value }
46
+ };
47
+ }
48
+ if (rule.field && rule.op !== void 0) {
49
+ return {
50
+ type: "COMPARE",
51
+ operator: rule.op,
52
+ left: { type: "VARIABLE", path: rule.field },
53
+ right: { type: "LITERAL", value: rule.value }
54
+ };
55
+ }
56
+ if (typeof rule === "boolean") {
57
+ return { type: "LITERAL", value: rule };
58
+ }
59
+ throw new Error(`[ExpressionTree] Invalid rule node: ${JSON.stringify(rule)}`);
60
+ }
61
+ function evaluateNode(node, ctx) {
62
+ switch (node.type) {
63
+ case "AND":
64
+ return node.children.every((child) => evaluateNode(child, ctx));
65
+ case "OR":
66
+ return node.children.some((child) => evaluateNode(child, ctx));
67
+ case "NOT":
68
+ return !evaluateNode(node.child, ctx);
69
+ case "COMPARE":
70
+ return evaluateComparison(node, ctx);
71
+ case "LITERAL":
72
+ return Boolean(node.value);
73
+ case "VARIABLE":
74
+ return Boolean(resolveVariable(node.path, ctx));
75
+ case "FUNCTION":
76
+ return Boolean(evaluateFunction(node, ctx));
77
+ default:
78
+ return false;
79
+ }
80
+ }
81
+ function evaluateComparison(node, ctx) {
82
+ const leftVal = resolveNode(node.left, ctx);
83
+ const rightVal = resolveNode(node.right, ctx);
84
+ switch (node.operator) {
85
+ case "==":
86
+ return leftVal == rightVal;
87
+ case "!=":
88
+ return leftVal != rightVal;
89
+ case ">":
90
+ return Number(leftVal) > Number(rightVal);
91
+ case "<":
92
+ return Number(leftVal) < Number(rightVal);
93
+ case ">=":
94
+ return Number(leftVal) >= Number(rightVal);
95
+ case "<=":
96
+ return Number(leftVal) <= Number(rightVal);
97
+ case "in":
98
+ return Array.isArray(rightVal) ? rightVal.includes(leftVal) : false;
99
+ case "not_in":
100
+ return Array.isArray(rightVal) ? !rightVal.includes(leftVal) : true;
101
+ case "contains":
102
+ return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.includes(rightVal) : false;
103
+ case "starts_with":
104
+ return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.startsWith(rightVal) : false;
105
+ case "ends_with":
106
+ return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.endsWith(rightVal) : false;
107
+ case "matches":
108
+ try {
109
+ return typeof leftVal === "string" && typeof rightVal === "string" ? new RegExp(rightVal).test(leftVal) : false;
110
+ } catch {
111
+ return false;
112
+ }
113
+ default:
114
+ return false;
115
+ }
116
+ }
117
+ function resolveNode(node, ctx) {
118
+ switch (node.type) {
119
+ case "LITERAL":
120
+ return node.value;
121
+ case "VARIABLE":
122
+ return resolveVariable(node.path, ctx);
123
+ case "FUNCTION":
124
+ return evaluateFunction(node, ctx);
125
+ default:
126
+ return evaluateNode(node, ctx);
127
+ }
128
+ }
129
+ function resolveVariable(path, ctx) {
130
+ const parts = path.split(".");
131
+ let current = ctx;
132
+ for (const part of parts) {
133
+ if (current === null || current === void 0) return void 0;
134
+ current = current[part];
135
+ }
136
+ return current;
137
+ }
138
+ var BUILT_IN_FUNCTIONS = {
139
+ now_hour: () => (/* @__PURE__ */ new Date()).getHours(),
140
+ now_day: () => (/* @__PURE__ */ new Date()).getDay(),
141
+ // 0=Sunday
142
+ now_timestamp: () => Date.now(),
143
+ len: (args) => {
144
+ const val = args[0];
145
+ return typeof val === "string" || Array.isArray(val) ? val.length : 0;
146
+ },
147
+ lower: (args) => String(args[0] || "").toLowerCase(),
148
+ upper: (args) => String(args[0] || "").toUpperCase(),
149
+ abs: (args) => Math.abs(Number(args[0] || 0)),
150
+ min: (args) => Math.min(...args.map(Number)),
151
+ max: (args) => Math.max(...args.map(Number)),
152
+ coalesce: (args) => args.find((a) => a !== null && a !== void 0),
153
+ risk_level: (args) => {
154
+ const score = Number(args[0] || 0);
155
+ if (score >= 0.9) return "critical";
156
+ if (score >= 0.7) return "high";
157
+ if (score >= 0.4) return "medium";
158
+ return "low";
159
+ },
160
+ is_business_hours: () => {
161
+ const hour = (/* @__PURE__ */ new Date()).getHours();
162
+ const day = (/* @__PURE__ */ new Date()).getDay();
163
+ return day >= 1 && day <= 5 && hour >= 9 && hour < 18;
164
+ }
165
+ };
166
+ function registerFunction(name, fn) {
167
+ BUILT_IN_FUNCTIONS[name] = fn;
168
+ }
169
+ function evaluateFunction(node, ctx) {
170
+ const fn = BUILT_IN_FUNCTIONS[node.name];
171
+ if (!fn) {
172
+ console.warn(`[ExpressionTree] Unknown function: ${node.name}`);
173
+ return null;
174
+ }
175
+ const resolvedArgs = node.args.map((arg) => resolveNode(arg, ctx));
176
+ return fn(resolvedArgs, ctx);
177
+ }
178
+ function extractVariables(node) {
179
+ const vars = /* @__PURE__ */ new Set();
180
+ function walk(n) {
181
+ switch (n.type) {
182
+ case "VARIABLE":
183
+ vars.add(n.path);
184
+ break;
185
+ case "AND":
186
+ case "OR":
187
+ n.children.forEach(walk);
188
+ break;
189
+ case "NOT":
190
+ walk(n.child);
191
+ break;
192
+ case "COMPARE":
193
+ walk(n.left);
194
+ walk(n.right);
195
+ break;
196
+ case "FUNCTION":
197
+ n.args.forEach(walk);
198
+ break;
199
+ }
200
+ }
201
+ walk(node);
202
+ return Array.from(vars);
203
+ }
204
+ function nodeToString(node, depth = 0) {
205
+ const indent = " ".repeat(depth);
206
+ switch (node.type) {
207
+ case "AND":
208
+ return `${indent}AND(
209
+ ${node.children.map((c) => nodeToString(c, depth + 1)).join(",\n")}
210
+ ${indent})`;
211
+ case "OR":
212
+ return `${indent}OR(
213
+ ${node.children.map((c) => nodeToString(c, depth + 1)).join(",\n")}
214
+ ${indent})`;
215
+ case "NOT":
216
+ return `${indent}NOT(
217
+ ${nodeToString(node.child, depth + 1)}
218
+ ${indent})`;
219
+ case "COMPARE":
220
+ return `${indent}${nodeToString(node.left, 0)} ${node.operator} ${nodeToString(node.right, 0)}`;
221
+ case "LITERAL":
222
+ return `${indent}${JSON.stringify(node.value)}`;
223
+ case "VARIABLE":
224
+ return `${indent}\${${node.path}}`;
225
+ case "FUNCTION":
226
+ return `${indent}${node.name}(${node.args.map((a) => nodeToString(a, 0)).join(", ")})`;
227
+ default:
228
+ return `${indent}???`;
229
+ }
230
+ }
231
+ var ruleCache = /* @__PURE__ */ new Map();
232
+ function compileRuleSet(rules) {
233
+ ruleCache.clear();
234
+ for (const rule of rules) {
235
+ try {
236
+ ruleCache.set(rule.id, compileFromJSON(rule.condition));
237
+ } catch (err) {
238
+ console.error(`[ExpressionTree] Failed to compile rule ${rule.id}: ${rule.name}`, err);
239
+ }
240
+ }
241
+ }
242
+ function evaluateRules(rules, ctx) {
243
+ const sorted = [...rules].sort((a, b) => b.priority - a.priority);
244
+ const results = [];
245
+ let matched = null;
246
+ for (const rule of sorted) {
247
+ const start = performance.now();
248
+ const compiled = ruleCache.get(rule.id) || compileFromJSON(rule.condition);
249
+ let isMatch = false;
250
+ try {
251
+ isMatch = compiled.evaluate(ctx);
252
+ } catch (err) {
253
+ console.warn(`[ExpressionTree] Rule ${rule.id} evaluation error`, err);
254
+ }
255
+ const elapsed = performance.now() - start;
256
+ results.push({
257
+ ruleId: rule.id,
258
+ ruleName: rule.name,
259
+ matched: isMatch,
260
+ action: rule.action,
261
+ evaluationTimeMs: elapsed
262
+ });
263
+ if (isMatch && !matched) {
264
+ matched = rule;
265
+ }
266
+ }
267
+ return { matched, results };
268
+ }
269
+
270
+ // src/adaptiveThreshold.ts
271
+ var DEFAULT_THRESHOLDS = [
272
+ {
273
+ name: "risk_score",
274
+ currentValue: 0.7,
275
+ minValue: 0.3,
276
+ maxValue: 0.95,
277
+ alpha: 0.1,
278
+ maxStepPercent: 5,
279
+ lastAdjustedAt: 0,
280
+ directionLock: null,
281
+ consecutiveAdjustments: 0
282
+ },
283
+ {
284
+ name: "cost_limit_usd",
285
+ currentValue: 100,
286
+ minValue: 10,
287
+ maxValue: 1e4,
288
+ alpha: 0.05,
289
+ maxStepPercent: 10,
290
+ lastAdjustedAt: 0,
291
+ directionLock: null,
292
+ consecutiveAdjustments: 0
293
+ },
294
+ {
295
+ name: "latency_p99_ms",
296
+ currentValue: 5e3,
297
+ minValue: 1e3,
298
+ maxValue: 3e4,
299
+ alpha: 0.15,
300
+ maxStepPercent: 15,
301
+ lastAdjustedAt: 0,
302
+ directionLock: null,
303
+ consecutiveAdjustments: 0
304
+ },
305
+ {
306
+ name: "hallucination_score",
307
+ currentValue: 0.5,
308
+ minValue: 0.1,
309
+ maxValue: 0.9,
310
+ alpha: 0.08,
311
+ maxStepPercent: 5,
312
+ lastAdjustedAt: 0,
313
+ directionLock: null,
314
+ consecutiveAdjustments: 0
315
+ }
316
+ ];
317
+ var AdaptiveThresholdManager = class {
318
+ thresholds = /* @__PURE__ */ new Map();
319
+ feedbackBuffer = [];
320
+ maxBufferSize;
321
+ cooldownMs;
322
+ onAdjustment;
323
+ constructor(options = {}) {
324
+ this.maxBufferSize = options.maxBufferSize ?? 1e4;
325
+ this.cooldownMs = options.adjustmentCooldownMs ?? 5 * 60 * 1e3;
326
+ this.onAdjustment = options.onAdjustment;
327
+ const configs = options.thresholds ?? DEFAULT_THRESHOLDS;
328
+ for (const config of configs) {
329
+ this.thresholds.set(config.name, { ...config });
330
+ }
331
+ }
332
+ // ========== Core API ==========
333
+ /** Get a threshold by name */
334
+ getThreshold(name) {
335
+ const t = this.thresholds.get(name);
336
+ return t ? { ...t } : void 0;
337
+ }
338
+ /** Get all thresholds */
339
+ getAllThresholds() {
340
+ return Array.from(this.thresholds.values()).map((t) => ({ ...t }));
341
+ }
342
+ /** Record a decision feedback sample */
343
+ recordFeedback(feedback) {
344
+ this.feedbackBuffer.push(feedback);
345
+ if (this.feedbackBuffer.length > this.maxBufferSize) {
346
+ this.feedbackBuffer.splice(0, this.feedbackBuffer.length - this.maxBufferSize);
347
+ }
348
+ }
349
+ /**
350
+ * Adjust a single threshold based on feedback (EWMA algorithm).
351
+ * Returns null if no adjustment is needed (cooldown, insufficient samples, etc.).
352
+ */
353
+ async adjustThreshold(thresholdName, windowHours = 24) {
354
+ const config = this.thresholds.get(thresholdName);
355
+ if (!config) return null;
356
+ if (Date.now() - config.lastAdjustedAt < this.cooldownMs) return null;
357
+ const windowStart = Date.now() - windowHours * 60 * 60 * 1e3;
358
+ const windowFeedback = this.feedbackBuffer.filter(
359
+ (f) => f.thresholdName === thresholdName && f.timestamp >= windowStart
360
+ );
361
+ if (windowFeedback.length < 10) return null;
362
+ const fp = windowFeedback.filter((f) => f.outcome === "false_positive").length;
363
+ const fn = windowFeedback.filter((f) => f.outcome === "false_negative").length;
364
+ const total = windowFeedback.length;
365
+ const fpRate = fp / total;
366
+ const fnRate = fn / total;
367
+ let direction = "none";
368
+ let reason = "";
369
+ if (fpRate > 0.15 && fpRate > fnRate * 2) {
370
+ direction = "up";
371
+ reason = `False positive rate ${(fpRate * 100).toFixed(1)}% exceeds 15% threshold`;
372
+ } else if (fnRate > 0.1 && fnRate > fpRate * 2) {
373
+ direction = "down";
374
+ reason = `False negative rate ${(fnRate * 100).toFixed(1)}% exceeds 10% threshold`;
375
+ }
376
+ if (direction === "none") return null;
377
+ if (config.directionLock && config.directionLock !== direction && config.consecutiveAdjustments < 3) {
378
+ return null;
379
+ }
380
+ const errorRate = direction === "up" ? fpRate : fnRate;
381
+ const ewmaAdjustment = config.alpha * errorRate;
382
+ const maxStep = config.currentValue * (config.maxStepPercent / 100);
383
+ const actualStep = Math.min(ewmaAdjustment * config.currentValue, maxStep);
384
+ const previousValue = config.currentValue;
385
+ let newValue = direction === "up" ? config.currentValue + actualStep : config.currentValue - actualStep;
386
+ newValue = Math.max(config.minValue, Math.min(config.maxValue, newValue));
387
+ config.currentValue = newValue;
388
+ config.lastAdjustedAt = Date.now();
389
+ config.consecutiveAdjustments = config.directionLock === direction ? config.consecutiveAdjustments + 1 : 1;
390
+ config.directionLock = direction;
391
+ const result = {
392
+ thresholdName,
393
+ previousValue,
394
+ newValue,
395
+ reason,
396
+ metrics: { falsePositiveRate: fpRate, falseNegativeRate: fnRate, totalSamples: total, windowHours }
397
+ };
398
+ if (this.onAdjustment) {
399
+ try {
400
+ await this.onAdjustment(result);
401
+ } catch (err) {
402
+ console.warn("[AdaptiveThreshold] Audit callback failed", err);
403
+ }
404
+ }
405
+ return result;
406
+ }
407
+ /** Adjust all thresholds */
408
+ async adjustAll(windowHours = 24) {
409
+ const results = [];
410
+ for (const [name] of this.thresholds) {
411
+ const result = await this.adjustThreshold(name, windowHours);
412
+ if (result) results.push(result);
413
+ }
414
+ return results;
415
+ }
416
+ /** Manually override a threshold value */
417
+ setThresholdManual(name, value) {
418
+ const config = this.thresholds.get(name);
419
+ if (!config) return false;
420
+ if (value < config.minValue || value > config.maxValue) return false;
421
+ config.currentValue = value;
422
+ config.lastAdjustedAt = Date.now();
423
+ config.directionLock = null;
424
+ config.consecutiveAdjustments = 0;
425
+ return true;
426
+ }
427
+ /** Get feedback statistics for a threshold (or all) */
428
+ getFeedbackStats(thresholdName, windowHours = 24) {
429
+ const windowStart = Date.now() - windowHours * 60 * 60 * 1e3;
430
+ const filtered = this.feedbackBuffer.filter(
431
+ (f) => f.timestamp >= windowStart && (!thresholdName || f.thresholdName === thresholdName)
432
+ );
433
+ const tp = filtered.filter((f) => f.outcome === "true_positive").length;
434
+ const tn = filtered.filter((f) => f.outcome === "true_negative").length;
435
+ const fp = filtered.filter((f) => f.outcome === "false_positive").length;
436
+ const fn = filtered.filter((f) => f.outcome === "false_negative").length;
437
+ const total = filtered.length;
438
+ return {
439
+ total,
440
+ truePositive: tp,
441
+ trueNegative: tn,
442
+ falsePositive: fp,
443
+ falseNegative: fn,
444
+ accuracy: total > 0 ? (tp + tn) / total : 0,
445
+ precision: tp + fp > 0 ? tp / (tp + fp) : 0,
446
+ recall: tp + fn > 0 ? tp / (tp + fn) : 0
447
+ };
448
+ }
449
+ /** Clear the feedback buffer */
450
+ clearFeedback() {
451
+ this.feedbackBuffer.length = 0;
452
+ }
453
+ };
454
+
455
+ // src/featureSwitches.ts
456
+ var SOVR_FEATURE_SWITCHES = {
457
+ USE_NEW_RISK_ENGINE: {
458
+ key: "USE_NEW_RISK_ENGINE",
459
+ description: "Enable new risk engine (expression tree + adaptive threshold), replacing legacy static rule matching",
460
+ defaultValue: false,
461
+ category: "risk",
462
+ impactLevel: "high"
463
+ },
464
+ ENFORCE_COST_CAP: {
465
+ key: "ENFORCE_COST_CAP",
466
+ description: "Enforce cost cap \u2014 hard-reject execution when agent cost exceeds budget (not just warn)",
467
+ defaultValue: false,
468
+ category: "cost",
469
+ impactLevel: "high"
470
+ },
471
+ ENABLE_PROMPT_GUARD: {
472
+ key: "ENABLE_PROMPT_GUARD",
473
+ description: "Enable prompt injection guard \u2014 scan all LLM call prompts for injection attacks",
474
+ defaultValue: true,
475
+ category: "security",
476
+ impactLevel: "medium"
477
+ },
478
+ DISABLE_EVIDENCE_PAUSE: {
479
+ key: "DISABLE_EVIDENCE_PAUSE",
480
+ description: "Disable evidence pause \u2014 keep audit chain writing continuously even under high system load",
481
+ defaultValue: false,
482
+ category: "evidence",
483
+ impactLevel: "medium"
484
+ }
485
+ };
486
+ var FeatureSwitchesManager = class {
487
+ cache = /* @__PURE__ */ new Map();
488
+ cacheTtlMs;
489
+ onLoad;
490
+ onSave;
491
+ constructor(options = {}) {
492
+ this.cacheTtlMs = options.cacheTtlMs ?? 3e4;
493
+ this.onLoad = options.onLoad;
494
+ this.onSave = options.onSave;
495
+ }
496
+ /**
497
+ * Get a switch value (with cache + optional external load).
498
+ */
499
+ async getValue(key, tenantId = "default") {
500
+ const cacheKey = `${tenantId}:${key}`;
501
+ const cached = this.cache.get(cacheKey);
502
+ if (cached && Date.now() - cached.updatedAt < this.cacheTtlMs) {
503
+ return cached.value;
504
+ }
505
+ if (this.onLoad) {
506
+ try {
507
+ const loaded = await this.onLoad(key, tenantId);
508
+ if (loaded !== null) {
509
+ this.cache.set(cacheKey, { value: loaded, updatedAt: Date.now() });
510
+ return loaded;
511
+ }
512
+ } catch (err) {
513
+ console.warn(`[FeatureSwitch] Failed to load ${key}, using default`, err);
514
+ }
515
+ }
516
+ const def = SOVR_FEATURE_SWITCHES[key];
517
+ const defaultVal = def ? def.defaultValue : false;
518
+ this.cache.set(cacheKey, { value: defaultVal, updatedAt: Date.now() });
519
+ return defaultVal;
520
+ }
521
+ /**
522
+ * Set a switch value (updates cache + optional external save).
523
+ */
524
+ async setValue(key, value, tenantId = "default") {
525
+ const def = SOVR_FEATURE_SWITCHES[key];
526
+ if (!def) {
527
+ throw new Error(`Unknown feature switch: ${key}`);
528
+ }
529
+ const cacheKey = `${tenantId}:${key}`;
530
+ if (this.onSave) {
531
+ await this.onSave(key, value, tenantId);
532
+ }
533
+ this.cache.set(cacheKey, { value, updatedAt: Date.now() });
534
+ }
535
+ /**
536
+ * Get all switch states.
537
+ */
538
+ async getAll(tenantId = "default") {
539
+ const result = {};
540
+ for (const [key, def] of Object.entries(SOVR_FEATURE_SWITCHES)) {
541
+ const value = await this.getValue(key, tenantId);
542
+ result[key] = {
543
+ key,
544
+ value,
545
+ description: def.description,
546
+ category: def.category,
547
+ impactLevel: def.impactLevel,
548
+ isDefault: value === def.defaultValue
549
+ };
550
+ }
551
+ return result;
552
+ }
553
+ /** Clear the cache (for testing or forced refresh) */
554
+ clearCache() {
555
+ this.cache.clear();
556
+ }
557
+ // ========== Convenience Checkers ==========
558
+ async isNewRiskEngineEnabled(tenantId) {
559
+ return this.getValue("USE_NEW_RISK_ENGINE", tenantId);
560
+ }
561
+ async isCostCapEnforced(tenantId) {
562
+ return this.getValue("ENFORCE_COST_CAP", tenantId);
563
+ }
564
+ async isPromptGuardEnabled(tenantId) {
565
+ return this.getValue("ENABLE_PROMPT_GUARD", tenantId);
566
+ }
567
+ async isEvidencePauseDisabled(tenantId) {
568
+ return this.getValue("DISABLE_EVIDENCE_PAUSE", tenantId);
569
+ }
570
+ };
571
+
572
+ // src/pricingRules.ts
573
+ var PricingRulesEngine = class {
574
+ rules = [];
575
+ constructor(rules) {
576
+ if (rules) {
577
+ this.loadRules(rules);
578
+ }
579
+ }
580
+ /** Load rules (sorted by priority descending) */
581
+ loadRules(rules) {
582
+ this.rules = [...rules].filter((r) => r.isActive).sort((a, b) => b.priority - a.priority);
583
+ }
584
+ /** Add a single rule */
585
+ addRule(rule) {
586
+ this.rules.push(rule);
587
+ this.rules.sort((a, b) => b.priority - a.priority);
588
+ }
589
+ /** Get all loaded rules */
590
+ getRules() {
591
+ return this.rules;
592
+ }
593
+ /**
594
+ * Evaluate pricing for an action.
595
+ * Returns the computed cost and breakdown.
596
+ */
597
+ evaluate(request) {
598
+ const currentHour = request.currentHour ?? (/* @__PURE__ */ new Date()).getHours();
599
+ for (const rule of this.rules) {
600
+ if (rule.tier && rule.tier !== request.tier) continue;
601
+ if (!this.globMatch(rule.actionPattern, request.action)) continue;
602
+ let timeWindowActive = false;
603
+ if (rule.timeWindowStart >= 0 && rule.timeWindowEnd >= 0) {
604
+ if (rule.timeWindowStart <= rule.timeWindowEnd) {
605
+ timeWindowActive = currentHour >= rule.timeWindowStart && currentHour < rule.timeWindowEnd;
606
+ } else {
607
+ timeWindowActive = currentHour >= rule.timeWindowStart || currentHour < rule.timeWindowEnd;
608
+ }
609
+ if (!timeWindowActive) continue;
610
+ } else {
611
+ timeWindowActive = false;
612
+ }
613
+ let cost = rule.baseCostUsd * rule.multiplier;
614
+ let discountPercent = 0;
615
+ if (rule.volumeDiscountThreshold > 0 && request.currentVolume >= rule.volumeDiscountThreshold) {
616
+ discountPercent = rule.volumeDiscountPercent;
617
+ cost = cost * (1 - discountPercent / 100);
618
+ }
619
+ const parts = [`base=$${rule.baseCostUsd.toFixed(4)}`];
620
+ if (rule.multiplier !== 1) parts.push(`\xD7${rule.multiplier}`);
621
+ if (discountPercent > 0) parts.push(`-${discountPercent}% vol`);
622
+ if (timeWindowActive) parts.push(`[time:${rule.timeWindowStart}-${rule.timeWindowEnd}]`);
623
+ return {
624
+ costUsd: Math.max(0, cost),
625
+ baseCostUsd: rule.baseCostUsd,
626
+ multiplier: rule.multiplier,
627
+ volumeDiscountPercent: discountPercent,
628
+ timeWindowActive,
629
+ matchedRuleId: rule.id,
630
+ breakdown: parts.join(" ")
631
+ };
632
+ }
633
+ return {
634
+ costUsd: 0,
635
+ baseCostUsd: 0,
636
+ multiplier: 1,
637
+ volumeDiscountPercent: 0,
638
+ timeWindowActive: false,
639
+ matchedRuleId: null,
640
+ breakdown: "no matching pricing rule"
641
+ };
642
+ }
643
+ globMatch(pattern, value) {
644
+ if (pattern === "*") return true;
645
+ const regex = new RegExp(
646
+ "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
647
+ );
648
+ return regex.test(value);
649
+ }
650
+ };
651
+
1
652
  // src/index.ts
2
653
  var DEFAULT_RULES = [
3
654
  // --- HTTP Proxy: Dangerous outbound calls ---
@@ -232,7 +883,7 @@ var RISK_SCORES = {
232
883
  high: 70,
233
884
  critical: 95
234
885
  };
235
- var ENGINE_VERSION = "3.1.0";
886
+ var ENGINE_VERSION = "3.3.0";
236
887
  var ENGINE_VERSION_CHECK_URL = "https://api.sovr.inc/api/sovr/v1/version/check";
237
888
  var ENGINE_TIER_LIMITS = {
238
889
  free: { evaluationsPerMonth: 50, irreversibleAllowsPerMonth: 0 },
@@ -304,10 +955,24 @@ var PolicyEngine = class {
304
955
  Run: npm install @sovr/engine@latest
305
956
  `
306
957
  );
307
- this._tier = "free";
958
+ console.error(
959
+ `
960
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
961
+ \u2551 SOVR ENGINE VERSION UPGRADE REQUIRED \u2551
962
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
963
+ \u2551 Current: @sovr/engine@${ENGINE_VERSION.padEnd(39)}\u2551
964
+ \u2551 Required: @sovr/engine@${(data.minVersion || "latest").toString().padEnd(38)}\u2551
965
+ \u2551 Run: npm install @sovr/engine@latest \u2551
966
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
967
+ `
968
+ );
969
+ process.exit(1);
308
970
  }
309
971
  }
310
- } catch {
972
+ } catch (e) {
973
+ if (e?.message?.includes("UPGRADE") || e?.message?.includes("DEPRECATED")) {
974
+ process.exit(1);
975
+ }
311
976
  }
312
977
  }
313
978
  /** Set the current tier (e.g., after API key verification) */
@@ -472,7 +1137,15 @@ var PolicyEngine = class {
472
1137
  };
473
1138
  var index_default = PolicyEngine;
474
1139
  export {
1140
+ AdaptiveThresholdManager,
475
1141
  DEFAULT_RULES,
1142
+ FeatureSwitchesManager,
476
1143
  PolicyEngine,
477
- index_default as default
1144
+ PricingRulesEngine,
1145
+ SOVR_FEATURE_SWITCHES,
1146
+ compileFromJSON,
1147
+ compileRuleSet,
1148
+ index_default as default,
1149
+ evaluateRules,
1150
+ registerFunction
478
1151
  };