paygate-mcp 6.5.0 → 6.7.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/README.md CHANGED
@@ -92,6 +92,8 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
92
92
  - **Quota Status** — `GET /keys/quota-status?key=...` returns daily/monthly quota usage for any key — calls and credits used/remaining/limits, reset periods, quota source (per-key vs global vs none)
93
93
  - **Credit History** — `GET /keys/credit-history?key=...` returns per-key credit mutation log — tracks initial allocation, topups, transfers (in/out), auto-topups, with type/limit/since filters, balance-before/after on every entry, newest-first ordering, capped at 100 entries per key
94
94
  - **Spending Velocity** — `GET /keys/spending-velocity?key=...` returns credit burn rate and depletion forecast — credits/calls per hour/day, estimated depletion date, top tools by spend, configurable analysis window (1h–30d)
95
+ - **Key Comparison** — `GET /keys/compare?keys=pg_a,pg_b` returns side-by-side comparison of 2–10 keys — credits, usage, velocity, rate limits, status, metadata (namespace/group/tags) — with not-found key reporting
96
+ - **Key Health Score** — `GET /keys/health?key=...` returns composite health score (0–100) with weighted component breakdown: balance health (30%), quota utilization (25%), rate limit pressure (20%), error rate (25%) — status levels (healthy/good/caution/warning/critical), key issue detection (revoked/suspended/expired/expiring/zero credits), alias support
95
97
  - **Config Hot Reload** — `POST /config/reload` reloads pricing, rate limits, webhooks, quotas, and behavior flags from config file without server restart
96
98
  - **Webhook Events** — POST batched usage events to any URL for external billing/alerting
97
99
  - **Config File Mode** — Load all settings from a JSON file (`--config`)
@@ -1796,6 +1798,74 @@ curl "http://localhost:3402/keys/spending-velocity?key=pg_...&window=48" -H "X-A
1796
1798
 
1797
1799
  `estimatedDepletionDate` and `estimatedHoursRemaining` are `null` when there's no spending activity. `topTools` shows the 5 highest-spend tools from usage data.
1798
1800
 
1801
+ ### Key Comparison
1802
+
1803
+ `GET /keys/compare?keys=pg_a,pg_b,pg_c` returns side-by-side comparison of 2–10 keys:
1804
+
1805
+ ```bash
1806
+ curl "http://localhost:3402/keys/compare?keys=pg_abc,pg_xyz" -H "X-Admin-Key: YOUR_ADMIN_KEY"
1807
+ ```
1808
+
1809
+ **Response:**
1810
+
1811
+ ```json
1812
+ {
1813
+ "compared": 2,
1814
+ "keys": [
1815
+ {
1816
+ "key": "pg_abc123...",
1817
+ "name": "prod-agent",
1818
+ "status": "active",
1819
+ "credits": { "current": 750, "totalSpent": 250 },
1820
+ "usage": { "totalCalls": 50, "totalAllowed": 48, "totalDenied": 2 },
1821
+ "velocity": { "creditsPerHour": 12.5, "creditsPerDay": 300, "estimatedHoursRemaining": 60 },
1822
+ "rateLimit": { "used": 3, "limit": 60, "remaining": 57 },
1823
+ "metadata": { "namespace": "prod", "group": "team-a", "createdAt": "2026-02-01T00:00:00Z", "tags": { "env": "prod" } }
1824
+ },
1825
+ {
1826
+ "key": "pg_xyz789...",
1827
+ "name": "staging-agent",
1828
+ "status": "active",
1829
+ "credits": { "current": 200, "totalSpent": 800 },
1830
+ "usage": { "totalCalls": 120, "totalAllowed": 120, "totalDenied": 0 },
1831
+ "velocity": { "creditsPerHour": 8.3, "creditsPerDay": 200, "estimatedHoursRemaining": 24 },
1832
+ "rateLimit": { "used": 0, "limit": 60, "remaining": 60 },
1833
+ "metadata": { "namespace": "staging", "group": null, "createdAt": "2026-02-15T00:00:00Z", "tags": {} }
1834
+ }
1835
+ ]
1836
+ }
1837
+ ```
1838
+
1839
+ Keys not found are reported in a `notFound` array. Supports aliases. Maximum 10 keys per comparison.
1840
+
1841
+ ### Key Health Score
1842
+
1843
+ `GET /keys/health?key=...` returns a composite health score (0–100) with weighted component breakdown:
1844
+
1845
+ ```bash
1846
+ curl "http://localhost:3402/keys/health?key=pg_abc" -H "X-Admin-Key: YOUR_ADMIN_KEY"
1847
+ ```
1848
+
1849
+ **Response:**
1850
+
1851
+ ```json
1852
+ {
1853
+ "key": "pg_abc123...",
1854
+ "name": "prod-agent",
1855
+ "score": 72,
1856
+ "status": "caution",
1857
+ "issues": ["Key expires within 7 days", "Credits depleting rapidly"],
1858
+ "components": {
1859
+ "balance": { "score": 40, "risk": "warning", "weight": 0.30 },
1860
+ "quota": { "score": 85, "risk": "good", "weight": 0.25 },
1861
+ "rateLimit": { "score": 100, "risk": "healthy", "weight": 0.20 },
1862
+ "errorRate": { "score": 75, "risk": "good", "weight": 0.25 }
1863
+ }
1864
+ }
1865
+ ```
1866
+
1867
+ Components: **balance** (30%, hours until credit depletion), **quota** (25%, max utilization across daily/monthly limits), **rateLimit** (20%, current window usage), **errorRate** (25%, denied/total ratio). Status: healthy (≥90), good (≥75), caution (≥50), warning (≥25), critical (<25). Issues detect: revoked, suspended, expired, expiring soon, zero credits, rapid depletion. Supports aliases.
1868
+
1799
1869
  ### IP Allowlisting
1800
1870
 
1801
1871
  Restrict API keys to specific IP addresses or CIDR ranges:
package/dist/server.d.ts CHANGED
@@ -183,6 +183,8 @@ export declare class PayGateServer {
183
183
  private handleQuotaStatus;
184
184
  private handleCreditHistory;
185
185
  private handleSpendingVelocity;
186
+ private handleKeyComparison;
187
+ private handleKeyHealth;
186
188
  private handleSetAutoTopup;
187
189
  private handleBalance;
188
190
  private handleLimits;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAgB,eAAe,EAA0B,MAAM,MAAM,CAAC;AAI7E,OAAO,EAAE,aAAa,EAAkB,mBAAmB,EAAkB,MAAM,SAAS,CAAC;AAU7F,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,cAAc,EAAqD,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAmB,MAAM,SAAS,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAS,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,eAAe,EAA6B,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,aAAa,EAAqB,MAAM,UAAU,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAKrD,0EAA0E;AAC1E,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAErE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAsBvF;AAyCD,yCAAyC;AACzC,KAAK,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AAa5C,qBAAa,aAAa;IACxB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC1C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,aAAa,CAAqC;IAC1D,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAQ;IAC5C,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,2BAA2B;IAC3B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,mCAAmC;IACnC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,gCAAgC;IAChC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAAQ;IAC5C,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,oCAAoC;IACpC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,oDAAoD;IACpD,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC;IACvC,sCAAsC;IACtC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,yCAAyC;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,gEAAgE;IAChE,OAAO,CAAC,QAAQ,CAAS;IACzB,wCAAwC;IACxC,OAAO,CAAC,QAAQ,CAAK;IACrB,sEAAsE;IACtE,OAAO,CAAC,UAAU,CAAuB;IAEzC,0DAA0D;IAC1D,OAAO,KAAK,OAAO,GAElB;gBAGC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAC1D,QAAQ,CAAC,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,OAAO,CAAC,EAAE,mBAAmB,EAAE,EAC/B,QAAQ,CAAC,EAAE,MAAM;IAyLnB;;;OAGG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIjC;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAK1B,KAAK,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;YAsC5C,aAAa;YA+Pb,SAAS;IA4NvB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IA+C1B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAyB9B;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAyCrB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAuC7B,OAAO,CAAC,UAAU;IAoGlB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,YAAY;IAyCpB,OAAO,CAAC,UAAU;IAuElB,OAAO,CAAC,kBAAkB;IA0D1B,kEAAkE;IAClE,OAAO,CAAC,OAAO;YAWD,eAAe;IAqH7B,OAAO,CAAC,cAAc;YA0CR,WAAW;YAuEX,oBAAoB;YAwHpB,oBAAoB;IA4IlC,OAAO,CAAC,eAAe;YAoDT,eAAe;YAsEf,eAAe;YAsDf,gBAAgB;YAkEhB,eAAe;YAgEf,cAAc;YAuFd,cAAc;YAoEd,eAAe;YA0Df,YAAY;YAkDZ,eAAe;YAwDf,cAAc;YA+Dd,aAAa;YAsDb,oBAAoB;YAsDpB,qBAAqB;IAgCnC,OAAO,CAAC,cAAc;IA2CtB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,cAAc;IAyEtB,OAAO,CAAC,qBAAqB;IAsD7B,OAAO,CAAC,iBAAiB;IAuEzB,OAAO,CAAC,mBAAmB;IA8C3B,OAAO,CAAC,sBAAsB;IAwD9B,OAAO,CAAC,mBAAmB;IAoG3B,OAAO,CAAC,eAAe;YAiJT,kBAAkB;IAoFhC,OAAO,CAAC,aAAa;YAuDP,YAAY;IAkD1B,OAAO,CAAC,WAAW;YA+CL,mBAAmB;IAmCjC,OAAO,CAAC,eAAe;IAYvB,+EAA+E;IAC/E,OAAO,CAAC,mBAAmB;IAU3B,oEAAoE;YACtD,mBAAmB;IA4DjC,yDAAyD;YAC3C,oBAAoB;IAuFlC,yCAAyC;YAC3B,gBAAgB;IA8E9B,uDAAuD;YACzC,iBAAiB;IAiC/B,sEAAsE;IACtE,OAAO,CAAC,kBAAkB;IAqB1B,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,eAAe;IA0BvB,OAAO,CAAC,eAAe;YAYT,qBAAqB;IAmDnC,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,sBAAsB;YAwBhB,mBAAmB;YAoDnB,kBAAkB;IA4IhC,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,gBAAgB;IA6CxB,OAAO,CAAC,kBAAkB;IAgC1B,OAAO,CAAC,mBAAmB;YAiCb,iBAAiB;IA6H/B,OAAO,CAAC,wBAAwB;YAclB,yBAAyB;YAsCzB,yBAAyB;YAiDzB,yBAAyB;IA4CvC,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,iBAAiB;IAgCzB,OAAO,CAAC,gBAAgB;IAcxB,OAAO,CAAC,UAAU;IAiClB,OAAO,CAAC,eAAe;YAiBT,gBAAgB;YA4ChB,gBAAgB;YA6ChB,gBAAgB;YAsChB,mBAAmB;YAsDnB,mBAAmB;IA8CjC,OAAO,CAAC,eAAe;IA8BvB,OAAO,CAAC,oBAAoB;YAgBd,iBAAiB;YAyDjB,iBAAiB;IAiE/B,OAAO,CAAC,uBAAuB;IAyB/B,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,gBAAgB;YAOV,iBAAiB;YA2CjB,iBAAiB;YAuDjB,iBAAiB;YAyCjB,sBAAsB;YAsDtB,wBAAwB;IAiDtC,OAAO,CAAC,mBAAmB;YAsBb,oBAAoB;YAwDpB,oBAAoB;IAwDlC,OAAO,CAAC,mBAAmB;YAQb,oBAAoB;YAsCpB,oBAAoB;IAuClC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,eAAe;IAUvB,iFAAiF;IACjF,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,QAAQ;IAkBV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB3B;;;;;;;OAOG;IACG,YAAY,CAAC,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CA6CtD"}
package/dist/server.js CHANGED
@@ -540,6 +540,10 @@ class PayGateServer {
540
540
  return this.handleCreditHistory(req, res);
541
541
  case '/keys/spending-velocity':
542
542
  return this.handleSpendingVelocity(req, res);
543
+ case '/keys/compare':
544
+ return this.handleKeyComparison(req, res);
545
+ case '/keys/health':
546
+ return this.handleKeyHealth(req, res);
543
547
  case '/keys/templates':
544
548
  if (req.method === 'GET')
545
549
  return this.handleListTemplates(req, res);
@@ -1087,6 +1091,8 @@ class PayGateServer {
1087
1091
  quotaStatus: 'GET /keys/quota-status?key=... — Current daily/monthly quota usage (requires X-Admin-Key)',
1088
1092
  creditHistory: 'GET /keys/credit-history?key=... — Per-key credit mutation history (requires X-Admin-Key)',
1089
1093
  spendingVelocity: 'GET /keys/spending-velocity?key=... — Spending rate and depletion forecast (requires X-Admin-Key)',
1094
+ keyComparison: 'GET /keys/compare?keys=pg_a,pg_b — Side-by-side key comparison (requires X-Admin-Key)',
1095
+ keyHealth: 'GET /keys/health?key=... — Composite health score (0-100) with component breakdown (requires X-Admin-Key)',
1090
1096
  keyTemplates: 'GET /keys/templates — List key templates + POST to create/update (requires X-Admin-Key)',
1091
1097
  deleteTemplate: 'POST /keys/templates/delete — Delete a key template (requires X-Admin-Key)',
1092
1098
  pricing: 'GET /pricing — Tool pricing breakdown (public)',
@@ -2829,6 +2835,263 @@ class PayGateServer {
2829
2835
  topTools,
2830
2836
  }));
2831
2837
  }
2838
+ // ─── /keys/compare — Side-by-side key comparison ────────────────────────────
2839
+ handleKeyComparison(req, res) {
2840
+ if (req.method !== 'GET') {
2841
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2842
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
2843
+ return;
2844
+ }
2845
+ if (!this.checkAdmin(req, res))
2846
+ return;
2847
+ const urlParts = req.url?.split('?') || [];
2848
+ const params = new URLSearchParams(urlParts[1] || '');
2849
+ const keysParam = params.get('keys');
2850
+ if (!keysParam) {
2851
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2852
+ res.end(JSON.stringify({ error: 'Missing required query parameter: keys (comma-separated)' }));
2853
+ return;
2854
+ }
2855
+ const keyIds = keysParam.split(',').map(k => k.trim()).filter(Boolean);
2856
+ if (keyIds.length < 2) {
2857
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2858
+ res.end(JSON.stringify({ error: 'At least 2 keys required for comparison' }));
2859
+ return;
2860
+ }
2861
+ if (keyIds.length > 10) {
2862
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2863
+ res.end(JSON.stringify({ error: 'Maximum 10 keys per comparison' }));
2864
+ return;
2865
+ }
2866
+ const comparisons = [];
2867
+ const notFound = [];
2868
+ for (const keyId of keyIds) {
2869
+ const resolved = this.gate.store.resolveKey(keyId);
2870
+ const actualKey = resolved ? resolved.key : keyId;
2871
+ const record = this.gate.store.getKey(actualKey);
2872
+ if (!record) {
2873
+ notFound.push(keyId);
2874
+ continue;
2875
+ }
2876
+ // Get usage stats
2877
+ const keyUsage = this.gate.meter.getKeyUsage(actualKey);
2878
+ // Get velocity (24h window)
2879
+ const velocity = this.creditLedger.getSpendingVelocity(actualKey, record.credits, 24);
2880
+ // Get rate limit status
2881
+ const rateStatus = this.gate.rateLimiter.getStatus(actualKey);
2882
+ // Determine key status
2883
+ let status = 'active';
2884
+ if (!record.active)
2885
+ status = 'revoked';
2886
+ else if (record.suspended)
2887
+ status = 'suspended';
2888
+ else if (record.expiresAt && new Date(record.expiresAt).getTime() < Date.now())
2889
+ status = 'expired';
2890
+ comparisons.push({
2891
+ key: actualKey.slice(0, 10) + '...',
2892
+ name: record.name,
2893
+ status,
2894
+ credits: {
2895
+ current: record.credits,
2896
+ totalSpent: keyUsage.totalCreditsSpent,
2897
+ },
2898
+ usage: {
2899
+ totalCalls: keyUsage.totalCalls,
2900
+ totalAllowed: keyUsage.totalAllowed,
2901
+ totalDenied: keyUsage.totalDenied,
2902
+ },
2903
+ velocity: {
2904
+ creditsPerHour: velocity.creditsPerHour,
2905
+ creditsPerDay: velocity.creditsPerDay,
2906
+ estimatedHoursRemaining: velocity.estimatedHoursRemaining,
2907
+ },
2908
+ rateLimit: {
2909
+ used: rateStatus.used,
2910
+ limit: rateStatus.limit,
2911
+ remaining: rateStatus.remaining,
2912
+ },
2913
+ metadata: {
2914
+ namespace: record.namespace || null,
2915
+ group: record.group || null,
2916
+ createdAt: record.createdAt,
2917
+ tags: record.tags || {},
2918
+ },
2919
+ });
2920
+ }
2921
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2922
+ res.end(JSON.stringify({
2923
+ compared: comparisons.length,
2924
+ notFound: notFound.length > 0 ? notFound : undefined,
2925
+ keys: comparisons,
2926
+ }));
2927
+ }
2928
+ // ─── /keys/health — Composite health score ──────────────────────────────────
2929
+ handleKeyHealth(req, res) {
2930
+ if (req.method !== 'GET') {
2931
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2932
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
2933
+ return;
2934
+ }
2935
+ if (!this.checkAdmin(req, res))
2936
+ return;
2937
+ const urlParts = req.url?.split('?') || [];
2938
+ const params = new URLSearchParams(urlParts[1] || '');
2939
+ const key = params.get('key');
2940
+ if (!key) {
2941
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2942
+ res.end(JSON.stringify({ error: 'Missing required query parameter: key' }));
2943
+ return;
2944
+ }
2945
+ // Use resolveKeyRaw to include expired/suspended/revoked keys in health check
2946
+ const record = this.gate.store.resolveKeyRaw(key);
2947
+ if (!record) {
2948
+ res.writeHead(404, { 'Content-Type': 'application/json' });
2949
+ res.end(JSON.stringify({ error: 'Key not found' }));
2950
+ return;
2951
+ }
2952
+ const actualKey = record.key;
2953
+ // ── Component 1: Balance health (30% weight) ──
2954
+ // Based on spending velocity → how many hours of usage remaining
2955
+ const velocity = this.creditLedger.getSpendingVelocity(actualKey, record.credits, 24);
2956
+ let balanceScore = 100;
2957
+ let balanceRisk = 'healthy';
2958
+ if (velocity.creditsPerHour > 0) {
2959
+ const hoursLeft = velocity.estimatedHoursRemaining ?? Infinity;
2960
+ if (hoursLeft <= 1) {
2961
+ balanceScore = 0;
2962
+ balanceRisk = 'critical';
2963
+ }
2964
+ else if (hoursLeft <= 6) {
2965
+ balanceScore = 20;
2966
+ balanceRisk = 'critical';
2967
+ }
2968
+ else if (hoursLeft <= 24) {
2969
+ balanceScore = 40;
2970
+ balanceRisk = 'warning';
2971
+ }
2972
+ else if (hoursLeft <= 72) {
2973
+ balanceScore = 60;
2974
+ balanceRisk = 'caution';
2975
+ }
2976
+ else if (hoursLeft <= 168) {
2977
+ balanceScore = 80;
2978
+ balanceRisk = 'good';
2979
+ }
2980
+ }
2981
+ else if (record.credits <= 0) {
2982
+ balanceScore = 0;
2983
+ balanceRisk = 'critical';
2984
+ }
2985
+ // ── Component 2: Quota utilization (25% weight) ──
2986
+ let quotaScore = 100;
2987
+ let quotaRisk = 'healthy';
2988
+ const quotaConfig = record.quota || this.config.globalQuota;
2989
+ if (quotaConfig) {
2990
+ // Reset counters if day/month rolled over before reading
2991
+ this.gate.quotaTracker.resetIfNeeded(record);
2992
+ let maxUtilization = 0;
2993
+ if (quotaConfig.dailyCallLimit && quotaConfig.dailyCallLimit > 0) {
2994
+ maxUtilization = Math.max(maxUtilization, record.quotaDailyCalls / quotaConfig.dailyCallLimit);
2995
+ }
2996
+ if (quotaConfig.monthlyCallLimit && quotaConfig.monthlyCallLimit > 0) {
2997
+ maxUtilization = Math.max(maxUtilization, record.quotaMonthlyCalls / quotaConfig.monthlyCallLimit);
2998
+ }
2999
+ if (quotaConfig.dailyCreditLimit && quotaConfig.dailyCreditLimit > 0) {
3000
+ maxUtilization = Math.max(maxUtilization, record.quotaDailyCredits / quotaConfig.dailyCreditLimit);
3001
+ }
3002
+ if (quotaConfig.monthlyCreditLimit && quotaConfig.monthlyCreditLimit > 0) {
3003
+ maxUtilization = Math.max(maxUtilization, record.quotaMonthlyCredits / quotaConfig.monthlyCreditLimit);
3004
+ }
3005
+ quotaScore = Math.max(0, Math.round((1 - maxUtilization) * 100));
3006
+ if (maxUtilization >= 1)
3007
+ quotaRisk = 'critical';
3008
+ else if (maxUtilization >= 0.9)
3009
+ quotaRisk = 'warning';
3010
+ else if (maxUtilization >= 0.75)
3011
+ quotaRisk = 'caution';
3012
+ else if (maxUtilization >= 0.5)
3013
+ quotaRisk = 'good';
3014
+ }
3015
+ // ── Component 3: Rate limit pressure (20% weight) ──
3016
+ const rateStatus = this.gate.rateLimiter.getStatus(actualKey);
3017
+ let rateLimitScore = 100;
3018
+ let rateLimitRisk = 'healthy';
3019
+ if (rateStatus.limit > 0) {
3020
+ const utilization = rateStatus.used / rateStatus.limit;
3021
+ rateLimitScore = Math.max(0, Math.round((1 - utilization) * 100));
3022
+ if (utilization >= 1)
3023
+ rateLimitRisk = 'critical';
3024
+ else if (utilization >= 0.9)
3025
+ rateLimitRisk = 'warning';
3026
+ else if (utilization >= 0.75)
3027
+ rateLimitRisk = 'caution';
3028
+ else if (utilization >= 0.5)
3029
+ rateLimitRisk = 'good';
3030
+ }
3031
+ // ── Component 4: Error rate (25% weight) ──
3032
+ const keyUsage = this.gate.meter.getKeyUsage(actualKey);
3033
+ let errorScore = 100;
3034
+ let errorRisk = 'healthy';
3035
+ if (keyUsage.totalCalls > 0) {
3036
+ const errorRate = keyUsage.totalDenied / keyUsage.totalCalls;
3037
+ errorScore = Math.max(0, Math.round((1 - errorRate) * 100));
3038
+ if (errorRate >= 0.5)
3039
+ errorRisk = 'critical';
3040
+ else if (errorRate >= 0.25)
3041
+ errorRisk = 'warning';
3042
+ else if (errorRate >= 0.1)
3043
+ errorRisk = 'caution';
3044
+ else if (errorRate > 0)
3045
+ errorRisk = 'good';
3046
+ }
3047
+ // ── Composite score (weighted) ──
3048
+ const overallScore = Math.round(balanceScore * 0.30 +
3049
+ quotaScore * 0.25 +
3050
+ rateLimitScore * 0.20 +
3051
+ errorScore * 0.25);
3052
+ let overallStatus = 'healthy';
3053
+ if (overallScore < 25)
3054
+ overallStatus = 'critical';
3055
+ else if (overallScore < 50)
3056
+ overallStatus = 'warning';
3057
+ else if (overallScore < 75)
3058
+ overallStatus = 'caution';
3059
+ else if (overallScore < 90)
3060
+ overallStatus = 'good';
3061
+ // Check key-level issues
3062
+ const issues = [];
3063
+ if (!record.active)
3064
+ issues.push('Key is revoked');
3065
+ if (record.suspended)
3066
+ issues.push('Key is suspended');
3067
+ if (record.expiresAt && new Date(record.expiresAt).getTime() < Date.now())
3068
+ issues.push('Key has expired');
3069
+ if (record.expiresAt) {
3070
+ const hoursToExpiry = (new Date(record.expiresAt).getTime() - Date.now()) / 3_600_000;
3071
+ if (hoursToExpiry > 0 && hoursToExpiry <= 24)
3072
+ issues.push('Key expires within 24 hours');
3073
+ else if (hoursToExpiry > 0 && hoursToExpiry <= 168)
3074
+ issues.push('Key expires within 7 days');
3075
+ }
3076
+ if (record.credits <= 0)
3077
+ issues.push('Zero credits remaining');
3078
+ if (balanceRisk === 'critical')
3079
+ issues.push('Credits depleting rapidly');
3080
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3081
+ res.end(JSON.stringify({
3082
+ key: actualKey.slice(0, 10) + '...',
3083
+ name: record.name,
3084
+ score: overallScore,
3085
+ status: overallStatus,
3086
+ issues: issues.length > 0 ? issues : undefined,
3087
+ components: {
3088
+ balance: { score: balanceScore, risk: balanceRisk, weight: 0.30 },
3089
+ quota: { score: quotaScore, risk: quotaRisk, weight: 0.25 },
3090
+ rateLimit: { score: rateLimitScore, risk: rateLimitRisk, weight: 0.20 },
3091
+ errorRate: { score: errorScore, risk: errorRisk, weight: 0.25 },
3092
+ },
3093
+ }));
3094
+ }
2832
3095
  // ─── /keys/auto-topup — Configure auto-topup ────────────────────────────────
2833
3096
  async handleSetAutoTopup(req, res) {
2834
3097
  if (req.method !== 'POST') {