@sovr/engine 3.6.0 → 4.0.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.js CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  SOVR_FEATURE_SWITCHES: () => SOVR_FEATURE_SWITCHES,
43
43
  SelfConstraintEngine: () => SelfConstraintEngine,
44
44
  SemanticDriftDetectorEngine: () => SemanticDriftDetectorEngine,
45
+ TIME_WINDOWS: () => TIME_WINDOWS,
45
46
  TimeSeriesAggregator: () => TimeSeriesAggregator,
46
47
  TrendAlertEngine: () => TrendAlertEngine,
47
48
  TwoPhaseRouter: () => TwoPhaseRouter,
@@ -4326,6 +4327,60 @@ var OverrideDriftAnalyzer = class {
4326
4327
  }
4327
4328
  };
4328
4329
 
4330
+ // src/cockpitDataService.ts
4331
+ var TIME_WINDOWS = {
4332
+ "1h": { label: "1 \u5C0F\u65F6", seconds: 3600 },
4333
+ "6h": { label: "6 \u5C0F\u65F6", seconds: 21600 },
4334
+ "24h": { label: "24 \u5C0F\u65F6", seconds: 86400 },
4335
+ "7d": { label: "7 \u5929", seconds: 604800 },
4336
+ "30d": { label: "30 \u5929", seconds: 2592e3 }
4337
+ };
4338
+ var collectors = [];
4339
+ function registerMetricCollector(collector) {
4340
+ const exists = collectors.findIndex((c) => c.name === collector.name);
4341
+ if (exists >= 0) {
4342
+ collectors[exists] = collector;
4343
+ } else {
4344
+ collectors.push(collector);
4345
+ }
4346
+ }
4347
+ registerMetricCollector({
4348
+ name: "gate_requests_total",
4349
+ category: "gate",
4350
+ unit: "req/min",
4351
+ collect: async () => 0
4352
+ });
4353
+ registerMetricCollector({
4354
+ name: "gate_block_rate",
4355
+ category: "gate",
4356
+ unit: "%",
4357
+ collect: async () => 0
4358
+ });
4359
+ registerMetricCollector({
4360
+ name: "audit_events_total",
4361
+ category: "audit",
4362
+ unit: "events/min",
4363
+ collect: async () => 0
4364
+ });
4365
+ registerMetricCollector({
4366
+ name: "trust_score",
4367
+ category: "trust",
4368
+ unit: "score",
4369
+ collect: async () => 85
4370
+ });
4371
+ registerMetricCollector({
4372
+ name: "budget_utilization",
4373
+ category: "budget",
4374
+ unit: "%",
4375
+ collect: async () => 0
4376
+ });
4377
+ registerMetricCollector({
4378
+ name: "avg_latency_ms",
4379
+ category: "performance",
4380
+ unit: "ms",
4381
+ collect: async () => 0
4382
+ });
4383
+
4329
4384
  // src/index.ts
4330
4385
  var DEFAULT_RULES = [
4331
4386
  // --- HTTP Proxy: Dangerous outbound calls ---
@@ -4560,8 +4615,11 @@ var RISK_SCORES = {
4560
4615
  high: 70,
4561
4616
  critical: 95
4562
4617
  };
4563
- var ENGINE_VERSION = "3.4.0";
4618
+ var ENGINE_VERSION = "4.0.0";
4564
4619
  var ENGINE_VERSION_CHECK_URL = "https://api.sovr.inc/api/sovr/v1/version/check";
4620
+ var DEFAULT_METERING_ENDPOINT = "https://sovr-ai-mkzgqqeh.manus.space/api/v1/metering/batch";
4621
+ var LOGIN_VALIDATE_URL = "https://sovr-ai-mkzgqqeh.manus.space/api/keys/validate";
4622
+ var IRREVERSIBLE_ACTION_REGEX = /^(DELETE|DROP|TRUNCATE|ALTER|UPDATE|INSERT|CREATE|GRANT|REVOKE|COPY|shell_exec|file_write|db_delete|db_update|db_execute|payment|deploy|publish)$/i;
4565
4623
  var ENGINE_TIER_LIMITS = {
4566
4624
  free: { evaluationsPerMonth: 50, irreversibleAllowsPerMonth: 0 },
4567
4625
  personal: { evaluationsPerMonth: 5e3, irreversibleAllowsPerMonth: 500 },
@@ -4578,6 +4636,28 @@ var PolicyEngine = class {
4578
4636
  _usage = { evaluations: 0, irreversibleAllows: 0, monthKey: "" };
4579
4637
  _apiKey;
4580
4638
  _versionChecked = false;
4639
+ // v4.0.0: BillingReporter
4640
+ _billingEnabled;
4641
+ _billingEndpoint;
4642
+ _billingBuffer = [];
4643
+ _billingFlushTimer = null;
4644
+ _billingFlushInterval;
4645
+ _billingBufferMax;
4646
+ _billingStats = {
4647
+ eventsReported: 0,
4648
+ eventsDropped: 0,
4649
+ bufferSize: 0,
4650
+ byType: {
4651
+ "gate.check": 0,
4652
+ "gate.block": 0,
4653
+ "irreversible.allowed": 0,
4654
+ "trust_bundle.issued": 0,
4655
+ "trust_bundle.exported": 0,
4656
+ "replay.requested": 0,
4657
+ "auditor.session": 0
4658
+ }
4659
+ };
4660
+ _loginValidated = false;
4581
4661
  constructor(config) {
4582
4662
  this._apiKey = config.apiKey || process.env.SOVR_API_KEY || "";
4583
4663
  if (!this._apiKey) {
@@ -4609,7 +4689,19 @@ var PolicyEngine = class {
4609
4689
  this._tier = config.tier ?? "free";
4610
4690
  const now = /* @__PURE__ */ new Date();
4611
4691
  this._usage.monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
4692
+ const billing = config.billing ?? {};
4693
+ this._billingEnabled = billing.enabled !== false;
4694
+ this._billingEndpoint = billing.meteringEndpoint ?? DEFAULT_METERING_ENDPOINT;
4695
+ this._billingFlushInterval = billing.flushIntervalMs ?? 1e4;
4696
+ this._billingBufferMax = billing.bufferMax ?? 500;
4612
4697
  this._asyncVersionCheck();
4698
+ this._asyncLoginValidation();
4699
+ if (this._billingEnabled) {
4700
+ this._billingFlushTimer = setInterval(() => {
4701
+ this._flushBillingBuffer().catch(() => {
4702
+ });
4703
+ }, this._billingFlushInterval);
4704
+ }
4613
4705
  }
4614
4706
  /** v3.1.0: Async version check — blocks evaluate() on first call if deprecated */
4615
4707
  async _asyncVersionCheck() {
@@ -4652,6 +4744,137 @@ var PolicyEngine = class {
4652
4744
  }
4653
4745
  }
4654
4746
  }
4747
+ // ============================================================================
4748
+ // v4.0.0: Mandatory Login Validation (Remote API Key → User Identity)
4749
+ // ============================================================================
4750
+ /**
4751
+ * Async login validation — verifies API Key is bound to a registered user.
4752
+ * Runs once at startup. On failure: logs warning but allows operation (fail-open).
4753
+ * On success: sets tier from server response.
4754
+ */
4755
+ async _asyncLoginValidation() {
4756
+ if (this._loginValidated) return;
4757
+ try {
4758
+ const res = await globalThis.fetch(LOGIN_VALIDATE_URL, {
4759
+ method: "GET",
4760
+ headers: {
4761
+ "Authorization": `Bearer ${this._apiKey}`,
4762
+ "X-SOVR-Source": "engine",
4763
+ "X-SOVR-Version": ENGINE_VERSION
4764
+ },
4765
+ signal: AbortSignal.timeout(8e3)
4766
+ });
4767
+ if (res.ok) {
4768
+ const data = await res.json();
4769
+ this._loginValidated = true;
4770
+ if (data.tier) {
4771
+ this._tier = data.tier;
4772
+ }
4773
+ if (data.valid === false) {
4774
+ console.error(
4775
+ "\n\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\n\u2551 SOVR LOGIN VALIDATION FAILED \u2551\n\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\n\u2551 Your API key is not bound to a registered user. \u2551\n\u2551 Please login at: https://sovr.inc/login \u2551\n\u2551 Then get a valid key: https://sovr.inc/dashboard/api-keys \u2551\n\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\n"
4776
+ );
4777
+ }
4778
+ } else if (res.status === 401 || res.status === 403) {
4779
+ console.error(
4780
+ `[SOVR ENGINE] Login validation failed (HTTP ${res.status}). API key may be invalid or expired.`
4781
+ );
4782
+ }
4783
+ } catch {
4784
+ }
4785
+ }
4786
+ /** Get login validation status */
4787
+ get loginValidated() {
4788
+ return this._loginValidated;
4789
+ }
4790
+ // ============================================================================
4791
+ // v4.0.0: BillingReporter — 7 event types, batch flush, quota check
4792
+ // ============================================================================
4793
+ /**
4794
+ * Record a billing event into the buffer.
4795
+ * Fire-and-forget — never blocks the main request flow.
4796
+ */
4797
+ recordBillingEvent(event_type, action, resource, verdict, metadata) {
4798
+ if (!this._billingEnabled) return;
4799
+ this._billingBuffer.push({
4800
+ event_type,
4801
+ action,
4802
+ resource,
4803
+ verdict,
4804
+ api_key: this._apiKey,
4805
+ timestamp: Date.now(),
4806
+ metadata
4807
+ });
4808
+ this._billingStats.byType[event_type] = (this._billingStats.byType[event_type] || 0) + 1;
4809
+ if (this._billingBuffer.length >= this._billingBufferMax) {
4810
+ this._flushBillingBuffer().catch(() => {
4811
+ });
4812
+ }
4813
+ }
4814
+ /**
4815
+ * Flush billing buffer to the metering endpoint.
4816
+ * Batch POST — fire-and-forget with re-queue on failure.
4817
+ */
4818
+ async _flushBillingBuffer() {
4819
+ if (this._billingBuffer.length === 0) return;
4820
+ const batch = this._billingBuffer.splice(0, this._billingBufferMax);
4821
+ try {
4822
+ const response = await globalThis.fetch(this._billingEndpoint, {
4823
+ method: "POST",
4824
+ headers: {
4825
+ "Content-Type": "application/json",
4826
+ "Authorization": `Bearer ${this._apiKey}`,
4827
+ "X-SOVR-Source": "engine",
4828
+ "X-SOVR-Version": ENGINE_VERSION
4829
+ },
4830
+ body: JSON.stringify({ events: batch }),
4831
+ signal: AbortSignal.timeout(5e3)
4832
+ });
4833
+ if (response.ok) {
4834
+ this._billingStats.eventsReported += batch.length;
4835
+ } else {
4836
+ throw new Error(`HTTP ${response.status}`);
4837
+ }
4838
+ } catch {
4839
+ if (this._billingBuffer.length < this._billingBufferMax * 2) {
4840
+ this._billingBuffer.unshift(...batch);
4841
+ } else {
4842
+ this._billingStats.eventsDropped += batch.length;
4843
+ }
4844
+ }
4845
+ }
4846
+ /**
4847
+ * Classify an evaluate() call into the appropriate billing event type.
4848
+ * Key pricing principle: "放行的不可逆动作" is the highest-price tax base.
4849
+ */
4850
+ _classifyBillingEvent(action, verdict) {
4851
+ if (verdict === "deny" || verdict === "block") {
4852
+ return "gate.block";
4853
+ }
4854
+ if ((verdict === "allow" || verdict === "escalate") && IRREVERSIBLE_ACTION_REGEX.test(action)) {
4855
+ return "irreversible.allowed";
4856
+ }
4857
+ return "gate.check";
4858
+ }
4859
+ /** Get billing statistics */
4860
+ get billingStats() {
4861
+ return {
4862
+ ...this._billingStats,
4863
+ bufferSize: this._billingBuffer.length
4864
+ };
4865
+ }
4866
+ /**
4867
+ * Flush remaining billing events and stop the timer.
4868
+ * Call this before process exit for clean shutdown.
4869
+ */
4870
+ async shutdown() {
4871
+ if (this._billingFlushTimer) {
4872
+ clearInterval(this._billingFlushTimer);
4873
+ this._billingFlushTimer = null;
4874
+ }
4875
+ await this._flushBillingBuffer().catch(() => {
4876
+ });
4877
+ }
4655
4878
  /** Set the current tier (e.g., after API key verification) */
4656
4879
  setTier(tier) {
4657
4880
  this._tier = tier;
@@ -4763,6 +4986,14 @@ var PolicyEngine = class {
4763
4986
  Promise.resolve(this.onAudit(event)).catch(() => {
4764
4987
  });
4765
4988
  }
4989
+ const billingType = this._classifyBillingEvent(request.action, verdict);
4990
+ this.recordBillingEvent(billingType, request.action, request.resource, verdict, {
4991
+ risk_score: riskScore,
4992
+ risk_level: riskLevel,
4993
+ decision_id: decisionId,
4994
+ channel: request.channel,
4995
+ matched_rules: matchedRules.length
4996
+ });
4766
4997
  if (this._tier === "free") {
4767
4998
  return {
4768
4999
  ...result,
@@ -4837,6 +5068,7 @@ var index_default = PolicyEngine;
4837
5068
  SOVR_FEATURE_SWITCHES,
4838
5069
  SelfConstraintEngine,
4839
5070
  SemanticDriftDetectorEngine,
5071
+ TIME_WINDOWS,
4840
5072
  TimeSeriesAggregator,
4841
5073
  TrendAlertEngine,
4842
5074
  TwoPhaseRouter,
package/dist/index.mjs CHANGED
@@ -4257,6 +4257,60 @@ var OverrideDriftAnalyzer = class {
4257
4257
  }
4258
4258
  };
4259
4259
 
4260
+ // src/cockpitDataService.ts
4261
+ var TIME_WINDOWS = {
4262
+ "1h": { label: "1 \u5C0F\u65F6", seconds: 3600 },
4263
+ "6h": { label: "6 \u5C0F\u65F6", seconds: 21600 },
4264
+ "24h": { label: "24 \u5C0F\u65F6", seconds: 86400 },
4265
+ "7d": { label: "7 \u5929", seconds: 604800 },
4266
+ "30d": { label: "30 \u5929", seconds: 2592e3 }
4267
+ };
4268
+ var collectors = [];
4269
+ function registerMetricCollector(collector) {
4270
+ const exists = collectors.findIndex((c) => c.name === collector.name);
4271
+ if (exists >= 0) {
4272
+ collectors[exists] = collector;
4273
+ } else {
4274
+ collectors.push(collector);
4275
+ }
4276
+ }
4277
+ registerMetricCollector({
4278
+ name: "gate_requests_total",
4279
+ category: "gate",
4280
+ unit: "req/min",
4281
+ collect: async () => 0
4282
+ });
4283
+ registerMetricCollector({
4284
+ name: "gate_block_rate",
4285
+ category: "gate",
4286
+ unit: "%",
4287
+ collect: async () => 0
4288
+ });
4289
+ registerMetricCollector({
4290
+ name: "audit_events_total",
4291
+ category: "audit",
4292
+ unit: "events/min",
4293
+ collect: async () => 0
4294
+ });
4295
+ registerMetricCollector({
4296
+ name: "trust_score",
4297
+ category: "trust",
4298
+ unit: "score",
4299
+ collect: async () => 85
4300
+ });
4301
+ registerMetricCollector({
4302
+ name: "budget_utilization",
4303
+ category: "budget",
4304
+ unit: "%",
4305
+ collect: async () => 0
4306
+ });
4307
+ registerMetricCollector({
4308
+ name: "avg_latency_ms",
4309
+ category: "performance",
4310
+ unit: "ms",
4311
+ collect: async () => 0
4312
+ });
4313
+
4260
4314
  // src/index.ts
4261
4315
  var DEFAULT_RULES = [
4262
4316
  // --- HTTP Proxy: Dangerous outbound calls ---
@@ -4491,8 +4545,11 @@ var RISK_SCORES = {
4491
4545
  high: 70,
4492
4546
  critical: 95
4493
4547
  };
4494
- var ENGINE_VERSION = "3.4.0";
4548
+ var ENGINE_VERSION = "4.0.0";
4495
4549
  var ENGINE_VERSION_CHECK_URL = "https://api.sovr.inc/api/sovr/v1/version/check";
4550
+ var DEFAULT_METERING_ENDPOINT = "https://sovr-ai-mkzgqqeh.manus.space/api/v1/metering/batch";
4551
+ var LOGIN_VALIDATE_URL = "https://sovr-ai-mkzgqqeh.manus.space/api/keys/validate";
4552
+ var IRREVERSIBLE_ACTION_REGEX = /^(DELETE|DROP|TRUNCATE|ALTER|UPDATE|INSERT|CREATE|GRANT|REVOKE|COPY|shell_exec|file_write|db_delete|db_update|db_execute|payment|deploy|publish)$/i;
4496
4553
  var ENGINE_TIER_LIMITS = {
4497
4554
  free: { evaluationsPerMonth: 50, irreversibleAllowsPerMonth: 0 },
4498
4555
  personal: { evaluationsPerMonth: 5e3, irreversibleAllowsPerMonth: 500 },
@@ -4509,6 +4566,28 @@ var PolicyEngine = class {
4509
4566
  _usage = { evaluations: 0, irreversibleAllows: 0, monthKey: "" };
4510
4567
  _apiKey;
4511
4568
  _versionChecked = false;
4569
+ // v4.0.0: BillingReporter
4570
+ _billingEnabled;
4571
+ _billingEndpoint;
4572
+ _billingBuffer = [];
4573
+ _billingFlushTimer = null;
4574
+ _billingFlushInterval;
4575
+ _billingBufferMax;
4576
+ _billingStats = {
4577
+ eventsReported: 0,
4578
+ eventsDropped: 0,
4579
+ bufferSize: 0,
4580
+ byType: {
4581
+ "gate.check": 0,
4582
+ "gate.block": 0,
4583
+ "irreversible.allowed": 0,
4584
+ "trust_bundle.issued": 0,
4585
+ "trust_bundle.exported": 0,
4586
+ "replay.requested": 0,
4587
+ "auditor.session": 0
4588
+ }
4589
+ };
4590
+ _loginValidated = false;
4512
4591
  constructor(config) {
4513
4592
  this._apiKey = config.apiKey || process.env.SOVR_API_KEY || "";
4514
4593
  if (!this._apiKey) {
@@ -4540,7 +4619,19 @@ var PolicyEngine = class {
4540
4619
  this._tier = config.tier ?? "free";
4541
4620
  const now = /* @__PURE__ */ new Date();
4542
4621
  this._usage.monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
4622
+ const billing = config.billing ?? {};
4623
+ this._billingEnabled = billing.enabled !== false;
4624
+ this._billingEndpoint = billing.meteringEndpoint ?? DEFAULT_METERING_ENDPOINT;
4625
+ this._billingFlushInterval = billing.flushIntervalMs ?? 1e4;
4626
+ this._billingBufferMax = billing.bufferMax ?? 500;
4543
4627
  this._asyncVersionCheck();
4628
+ this._asyncLoginValidation();
4629
+ if (this._billingEnabled) {
4630
+ this._billingFlushTimer = setInterval(() => {
4631
+ this._flushBillingBuffer().catch(() => {
4632
+ });
4633
+ }, this._billingFlushInterval);
4634
+ }
4544
4635
  }
4545
4636
  /** v3.1.0: Async version check — blocks evaluate() on first call if deprecated */
4546
4637
  async _asyncVersionCheck() {
@@ -4583,6 +4674,137 @@ var PolicyEngine = class {
4583
4674
  }
4584
4675
  }
4585
4676
  }
4677
+ // ============================================================================
4678
+ // v4.0.0: Mandatory Login Validation (Remote API Key → User Identity)
4679
+ // ============================================================================
4680
+ /**
4681
+ * Async login validation — verifies API Key is bound to a registered user.
4682
+ * Runs once at startup. On failure: logs warning but allows operation (fail-open).
4683
+ * On success: sets tier from server response.
4684
+ */
4685
+ async _asyncLoginValidation() {
4686
+ if (this._loginValidated) return;
4687
+ try {
4688
+ const res = await globalThis.fetch(LOGIN_VALIDATE_URL, {
4689
+ method: "GET",
4690
+ headers: {
4691
+ "Authorization": `Bearer ${this._apiKey}`,
4692
+ "X-SOVR-Source": "engine",
4693
+ "X-SOVR-Version": ENGINE_VERSION
4694
+ },
4695
+ signal: AbortSignal.timeout(8e3)
4696
+ });
4697
+ if (res.ok) {
4698
+ const data = await res.json();
4699
+ this._loginValidated = true;
4700
+ if (data.tier) {
4701
+ this._tier = data.tier;
4702
+ }
4703
+ if (data.valid === false) {
4704
+ console.error(
4705
+ "\n\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\n\u2551 SOVR LOGIN VALIDATION FAILED \u2551\n\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\n\u2551 Your API key is not bound to a registered user. \u2551\n\u2551 Please login at: https://sovr.inc/login \u2551\n\u2551 Then get a valid key: https://sovr.inc/dashboard/api-keys \u2551\n\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\n"
4706
+ );
4707
+ }
4708
+ } else if (res.status === 401 || res.status === 403) {
4709
+ console.error(
4710
+ `[SOVR ENGINE] Login validation failed (HTTP ${res.status}). API key may be invalid or expired.`
4711
+ );
4712
+ }
4713
+ } catch {
4714
+ }
4715
+ }
4716
+ /** Get login validation status */
4717
+ get loginValidated() {
4718
+ return this._loginValidated;
4719
+ }
4720
+ // ============================================================================
4721
+ // v4.0.0: BillingReporter — 7 event types, batch flush, quota check
4722
+ // ============================================================================
4723
+ /**
4724
+ * Record a billing event into the buffer.
4725
+ * Fire-and-forget — never blocks the main request flow.
4726
+ */
4727
+ recordBillingEvent(event_type, action, resource, verdict, metadata) {
4728
+ if (!this._billingEnabled) return;
4729
+ this._billingBuffer.push({
4730
+ event_type,
4731
+ action,
4732
+ resource,
4733
+ verdict,
4734
+ api_key: this._apiKey,
4735
+ timestamp: Date.now(),
4736
+ metadata
4737
+ });
4738
+ this._billingStats.byType[event_type] = (this._billingStats.byType[event_type] || 0) + 1;
4739
+ if (this._billingBuffer.length >= this._billingBufferMax) {
4740
+ this._flushBillingBuffer().catch(() => {
4741
+ });
4742
+ }
4743
+ }
4744
+ /**
4745
+ * Flush billing buffer to the metering endpoint.
4746
+ * Batch POST — fire-and-forget with re-queue on failure.
4747
+ */
4748
+ async _flushBillingBuffer() {
4749
+ if (this._billingBuffer.length === 0) return;
4750
+ const batch = this._billingBuffer.splice(0, this._billingBufferMax);
4751
+ try {
4752
+ const response = await globalThis.fetch(this._billingEndpoint, {
4753
+ method: "POST",
4754
+ headers: {
4755
+ "Content-Type": "application/json",
4756
+ "Authorization": `Bearer ${this._apiKey}`,
4757
+ "X-SOVR-Source": "engine",
4758
+ "X-SOVR-Version": ENGINE_VERSION
4759
+ },
4760
+ body: JSON.stringify({ events: batch }),
4761
+ signal: AbortSignal.timeout(5e3)
4762
+ });
4763
+ if (response.ok) {
4764
+ this._billingStats.eventsReported += batch.length;
4765
+ } else {
4766
+ throw new Error(`HTTP ${response.status}`);
4767
+ }
4768
+ } catch {
4769
+ if (this._billingBuffer.length < this._billingBufferMax * 2) {
4770
+ this._billingBuffer.unshift(...batch);
4771
+ } else {
4772
+ this._billingStats.eventsDropped += batch.length;
4773
+ }
4774
+ }
4775
+ }
4776
+ /**
4777
+ * Classify an evaluate() call into the appropriate billing event type.
4778
+ * Key pricing principle: "放行的不可逆动作" is the highest-price tax base.
4779
+ */
4780
+ _classifyBillingEvent(action, verdict) {
4781
+ if (verdict === "deny" || verdict === "block") {
4782
+ return "gate.block";
4783
+ }
4784
+ if ((verdict === "allow" || verdict === "escalate") && IRREVERSIBLE_ACTION_REGEX.test(action)) {
4785
+ return "irreversible.allowed";
4786
+ }
4787
+ return "gate.check";
4788
+ }
4789
+ /** Get billing statistics */
4790
+ get billingStats() {
4791
+ return {
4792
+ ...this._billingStats,
4793
+ bufferSize: this._billingBuffer.length
4794
+ };
4795
+ }
4796
+ /**
4797
+ * Flush remaining billing events and stop the timer.
4798
+ * Call this before process exit for clean shutdown.
4799
+ */
4800
+ async shutdown() {
4801
+ if (this._billingFlushTimer) {
4802
+ clearInterval(this._billingFlushTimer);
4803
+ this._billingFlushTimer = null;
4804
+ }
4805
+ await this._flushBillingBuffer().catch(() => {
4806
+ });
4807
+ }
4586
4808
  /** Set the current tier (e.g., after API key verification) */
4587
4809
  setTier(tier) {
4588
4810
  this._tier = tier;
@@ -4694,6 +4916,14 @@ var PolicyEngine = class {
4694
4916
  Promise.resolve(this.onAudit(event)).catch(() => {
4695
4917
  });
4696
4918
  }
4919
+ const billingType = this._classifyBillingEvent(request.action, verdict);
4920
+ this.recordBillingEvent(billingType, request.action, request.resource, verdict, {
4921
+ risk_score: riskScore,
4922
+ risk_level: riskLevel,
4923
+ decision_id: decisionId,
4924
+ channel: request.channel,
4925
+ matched_rules: matchedRules.length
4926
+ });
4697
4927
  if (this._tier === "free") {
4698
4928
  return {
4699
4929
  ...result,
@@ -4767,6 +4997,7 @@ export {
4767
4997
  SOVR_FEATURE_SWITCHES,
4768
4998
  SelfConstraintEngine,
4769
4999
  SemanticDriftDetectorEngine,
5000
+ TIME_WINDOWS,
4770
5001
  TimeSeriesAggregator,
4771
5002
  TrendAlertEngine,
4772
5003
  TwoPhaseRouter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovr/engine",
3
- "version": "3.6.0",
3
+ "version": "4.0.0",
4
4
  "description": "Unified Policy Engine for SOVR — the single decision plane for all proxy channels",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",