paygate-mcp 7.9.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -106,6 +106,8 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
106
106
  - **Tool Call Dry Run** — `POST /requests/dry-run` simulates a tool call without executing — checks key validity, ACL, rate limits, credits, and spending limits, returns predicted outcome with credits-after calculation and rate limit status
107
107
  - **Batch Dry Run** — `POST /requests/dry-run/batch` simulates multiple tool calls at once — aggregate credit check, per-tool ACL validation, spending limit, returns per-tool results with total credits required and credits-after
108
108
  - **Tool Availability** — `GET /tools/available?key=...` returns per-key tool availability with pricing, affordability (canAfford), ACL enforcement (accessible/denyReason), and per-tool + global rate limit status
109
+ - **Key Dashboard** — `GET /keys/dashboard?key=...` consolidated single-endpoint view with metadata, balance, health score, spending velocity, rate limits, quotas, usage summary, and recent activity timeline
110
+ - **Admin Notifications** — `GET /admin/notifications` scans all keys for actionable issues: expired/expiring keys, zero credits, credit depletion velocity, suspended keys, high error rates, and rate limit pressure — with severity filtering and priority sorting
109
111
  - **Config Hot Reload** — `POST /config/reload` reloads pricing, rate limits, webhooks, quotas, and behavior flags from config file without server restart
110
112
  - **Webhook Events** — POST batched usage events to any URL for external billing/alerting
111
113
  - **Config File Mode** — Load all settings from a JSON file (`--config`)
@@ -2330,6 +2332,103 @@ curl "http://localhost:3402/tools/available?key=pg_..." \
2330
2332
 
2331
2333
  Returns every discovered tool with: `accessible` (ACL check), `denyReason` (if blocked), `creditsPerCall`, `canAfford` (credits vs price), and per-tool `rateLimit` when configured. Includes global rate limit info. Supports alias keys. Works on suspended keys (informational). Read-only — does not deduct credits or increment rate counters.
2332
2334
 
2335
+ ### Key Dashboard
2336
+
2337
+ Get a consolidated overview of any API key in a single request:
2338
+
2339
+ ```bash
2340
+ curl "http://localhost:3402/keys/dashboard?key=pg_..." \
2341
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2342
+ ```
2343
+
2344
+ **Response:**
2345
+
2346
+ ```json
2347
+ {
2348
+ "key": "pg_c815...09a6",
2349
+ "name": "production-agent",
2350
+ "status": "active",
2351
+ "namespace": "prod",
2352
+ "balance": { "credits": 850, "totalSpent": 150, "totalAllocated": 1000, "spendingLimit": 500 },
2353
+ "health": { "score": 92, "status": "good" },
2354
+ "velocity": { "creditsPerHour": 6.2, "creditsPerDay": 149, "estimatedDepletionDate": "2025-02-03T..." },
2355
+ "rateLimits": { "global": { "limit": 60, "used": 12, "remaining": 48, "resetInMs": 35000 } },
2356
+ "quotas": { "source": "global", "daily": { "callsUsed": 24, "callsLimit": 100 }, "monthly": { "callsUsed": 340, "callsLimit": 5000 } },
2357
+ "usage": { "totalCalls": 340, "totalAllowed": 330, "totalDenied": 10, "totalCredits": 150 },
2358
+ "recentActivity": [{ "timestamp": "...", "event": "gate.allowed", "tool": "search", "credits": 5 }]
2359
+ }
2360
+ ```
2361
+
2362
+ Combines metadata (status/namespace/group/tags), balance (credits/spent/allocated/spendingLimit), health score (0-100 composite), spending velocity with depletion forecast, rate limit and quota status, usage summary, and last 10 audit events. Supports alias keys. Works on suspended/revoked/expired keys. Read-only.
2363
+
2364
+ ### Admin Notifications
2365
+
2366
+ Get actionable notifications about keys that need attention:
2367
+
2368
+ ```bash
2369
+ # Get all notifications
2370
+ curl http://localhost:3402/admin/notifications \
2371
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2372
+
2373
+ # Filter by severity
2374
+ curl "http://localhost:3402/admin/notifications?severity=critical" \
2375
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2376
+ ```
2377
+
2378
+ **Response:**
2379
+
2380
+ ```json
2381
+ {
2382
+ "total": 4,
2383
+ "critical": 2,
2384
+ "warning": 1,
2385
+ "info": 1,
2386
+ "notifications": [
2387
+ {
2388
+ "severity": "critical",
2389
+ "category": "zero_credits",
2390
+ "message": "Key has zero credits remaining",
2391
+ "key": "pg_c815...09a6",
2392
+ "keyName": "production-agent"
2393
+ },
2394
+ {
2395
+ "severity": "critical",
2396
+ "category": "key_expiring_soon",
2397
+ "message": "Key expires within 8 hours",
2398
+ "key": "pg_a3f1...b2e4",
2399
+ "keyName": "staging-agent",
2400
+ "details": { "expiresAt": "2026-02-27T08:00:00.000Z", "hoursRemaining": 7.5 }
2401
+ },
2402
+ {
2403
+ "severity": "warning",
2404
+ "category": "credits_depleting",
2405
+ "message": "Credits will deplete in ~18 hours at current rate",
2406
+ "key": "pg_d7e2...f1a3",
2407
+ "keyName": "batch-worker",
2408
+ "details": { "credits": 90, "creditsPerHour": 5.1, "estimatedHoursRemaining": 17.6 }
2409
+ },
2410
+ {
2411
+ "severity": "info",
2412
+ "category": "key_suspended",
2413
+ "message": "Key is suspended",
2414
+ "key": "pg_b4c9...e8d5",
2415
+ "keyName": "deprecated-agent"
2416
+ }
2417
+ ]
2418
+ }
2419
+ ```
2420
+
2421
+ **Notification categories:**
2422
+ - `key_expired` (critical) — Key has passed its expiry date
2423
+ - `key_expiring_soon` (critical <24h, warning <7d) — Key approaching expiry
2424
+ - `zero_credits` (critical) — Key has no credits remaining
2425
+ - `credits_depleting` (critical <6h, warning <24h) — Spending velocity predicts depletion
2426
+ - `key_suspended` (info) — Key is suspended
2427
+ - `high_error_rate` (critical ≥50%, warning ≥25%) — High denial rate (min 10 calls)
2428
+ - `rate_limit_pressure` (warning ≥90%) — Rate limit nearly exhausted
2429
+
2430
+ Notifications are sorted by severity (critical first). Revoked keys are excluded. A single key can appear in multiple notifications (e.g., zero credits AND expiring soon). Filter with `?severity=critical|warning|info`. Read-only.
2431
+
2333
2432
  ### IP Allowlisting
2334
2433
 
2335
2434
  Restrict API keys to specific IP addresses or CIDR ranges:
package/dist/server.d.ts CHANGED
@@ -211,6 +211,7 @@ export declare class PayGateServer {
211
211
  private handleSpendingVelocity;
212
212
  private handleKeyComparison;
213
213
  private handleKeyHealth;
214
+ private handleKeyDashboard;
214
215
  private handleSetAutoTopup;
215
216
  private handleBalance;
216
217
  private handleLimits;
@@ -241,6 +242,7 @@ export declare class PayGateServer {
241
242
  private handleGetMaintenance;
242
243
  private handleSetMaintenance;
243
244
  private handleAdminEventStream;
245
+ private handleAdminNotifications;
244
246
  private handleGetNotes;
245
247
  private handleAddNote;
246
248
  private handleDeleteNote;
@@ -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":"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"}
package/dist/server.js CHANGED
@@ -638,6 +638,12 @@ class PayGateServer {
638
638
  return this.handleKeyComparison(req, res);
639
639
  case '/keys/health':
640
640
  return this.handleKeyHealth(req, res);
641
+ case '/keys/dashboard':
642
+ if (req.method === 'GET')
643
+ return this.handleKeyDashboard(req, res);
644
+ res.writeHead(405, { 'Content-Type': 'application/json' });
645
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
646
+ return;
641
647
  case '/keys/templates':
642
648
  if (req.method === 'GET')
643
649
  return this.handleListTemplates(req, res);
@@ -792,6 +798,12 @@ class PayGateServer {
792
798
  return this.handleRevokeAdminKey(req, res);
793
799
  case '/admin/events':
794
800
  return this.handleAdminEventStream(req, res);
801
+ case '/admin/notifications':
802
+ if (req.method === 'GET')
803
+ return this.handleAdminNotifications(req, res);
804
+ res.writeHead(405, { 'Content-Type': 'application/json' });
805
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
806
+ return;
795
807
  // ─── Plugin endpoints ──────────────────────────────────────────────
796
808
  case '/plugins':
797
809
  return this.handleListPlugins(req, res);
@@ -1331,6 +1343,8 @@ class PayGateServer {
1331
1343
  requestDryRunBatch: 'POST /requests/dry-run/batch — Simulate multiple tool calls without executing (requires X-Admin-Key)',
1332
1344
  toolStats: 'GET /tools/stats — Per-tool call counts, success rates, latency, credits, and top consumers (requires X-Admin-Key)',
1333
1345
  toolAvailability: 'GET /tools/available?key=... — Per-key tool availability with pricing, affordability, and rate limit status (requires X-Admin-Key)',
1346
+ keyDashboard: 'GET /keys/dashboard?key=... — Consolidated key overview with metadata, balance, health, velocity, rate limits, quotas, and recent activity (requires X-Admin-Key)',
1347
+ adminNotifications: 'GET /admin/notifications — Actionable notifications for expiring keys, low credits, high error rates, and rate limit pressure (requires X-Admin-Key)',
1334
1348
  ...(this.oauth ? {
1335
1349
  oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
1336
1350
  oauthRegister: 'POST /oauth/register — Register OAuth client',
@@ -3289,6 +3303,188 @@ class PayGateServer {
3289
3303
  },
3290
3304
  }));
3291
3305
  }
3306
+ // ─── /keys/dashboard — Consolidated key overview ──────────────────────────────
3307
+ handleKeyDashboard(req, res) {
3308
+ if (!this.checkAdmin(req, res))
3309
+ return;
3310
+ const urlParts = req.url?.split('?') || [];
3311
+ const params = new URLSearchParams(urlParts[1] || '');
3312
+ const keyParam = params.get('key');
3313
+ if (!keyParam) {
3314
+ res.writeHead(400, { 'Content-Type': 'application/json' });
3315
+ res.end(JSON.stringify({ error: 'Missing required parameter: key' }));
3316
+ return;
3317
+ }
3318
+ const record = this.gate.store.resolveKeyRaw(keyParam);
3319
+ if (!record) {
3320
+ res.writeHead(404, { 'Content-Type': 'application/json' });
3321
+ res.end(JSON.stringify({ error: 'Key not found' }));
3322
+ return;
3323
+ }
3324
+ const actualKey = record.key;
3325
+ const maskedKey = actualKey.slice(0, 7) + '...' + actualKey.slice(-4);
3326
+ // ── Metadata ──
3327
+ const isExpired = record.expiresAt ? new Date(record.expiresAt).getTime() < Date.now() : false;
3328
+ let status = 'active';
3329
+ if (!record.active)
3330
+ status = 'revoked';
3331
+ else if (record.suspended)
3332
+ status = 'suspended';
3333
+ else if (isExpired)
3334
+ status = 'expired';
3335
+ const metadata = {
3336
+ key: maskedKey,
3337
+ name: record.name || null,
3338
+ status,
3339
+ namespace: record.namespace || null,
3340
+ group: record.group || null,
3341
+ createdAt: record.createdAt || null,
3342
+ ...(record.expiresAt ? { expiresAt: record.expiresAt } : {}),
3343
+ tags: record.tags && Object.keys(record.tags).length > 0 ? record.tags : undefined,
3344
+ };
3345
+ // ── Balance ──
3346
+ const balance = {
3347
+ credits: record.credits,
3348
+ totalSpent: record.totalSpent || 0,
3349
+ totalAllocated: record.credits + (record.totalSpent || 0),
3350
+ ...(record.spendingLimit ? { spendingLimit: record.spendingLimit } : {}),
3351
+ };
3352
+ // ── Health score (simplified from handleKeyHealth) ──
3353
+ const velocity = this.creditLedger.getSpendingVelocity(actualKey, record.credits, 24);
3354
+ let balanceScore = 100;
3355
+ if (velocity.creditsPerHour > 0) {
3356
+ const hoursLeft = velocity.estimatedHoursRemaining ?? Infinity;
3357
+ if (hoursLeft <= 1)
3358
+ balanceScore = 0;
3359
+ else if (hoursLeft <= 6)
3360
+ balanceScore = 20;
3361
+ else if (hoursLeft <= 24)
3362
+ balanceScore = 40;
3363
+ else if (hoursLeft <= 72)
3364
+ balanceScore = 60;
3365
+ else if (hoursLeft <= 168)
3366
+ balanceScore = 80;
3367
+ }
3368
+ else if (record.credits <= 0) {
3369
+ balanceScore = 0;
3370
+ }
3371
+ // Quota component
3372
+ let quotaScore = 100;
3373
+ const quotaConfig = record.quota || this.config.globalQuota;
3374
+ if (quotaConfig) {
3375
+ this.gate.quotaTracker.resetIfNeeded(record);
3376
+ let maxUtil = 0;
3377
+ if (quotaConfig.dailyCallLimit && quotaConfig.dailyCallLimit > 0)
3378
+ maxUtil = Math.max(maxUtil, record.quotaDailyCalls / quotaConfig.dailyCallLimit);
3379
+ if (quotaConfig.monthlyCallLimit && quotaConfig.monthlyCallLimit > 0)
3380
+ maxUtil = Math.max(maxUtil, record.quotaMonthlyCalls / quotaConfig.monthlyCallLimit);
3381
+ if (quotaConfig.dailyCreditLimit && quotaConfig.dailyCreditLimit > 0)
3382
+ maxUtil = Math.max(maxUtil, record.quotaDailyCredits / quotaConfig.dailyCreditLimit);
3383
+ if (quotaConfig.monthlyCreditLimit && quotaConfig.monthlyCreditLimit > 0)
3384
+ maxUtil = Math.max(maxUtil, record.quotaMonthlyCredits / quotaConfig.monthlyCreditLimit);
3385
+ quotaScore = Math.max(0, Math.round((1 - maxUtil) * 100));
3386
+ }
3387
+ // Rate limit component
3388
+ const rateStatus = this.gate.rateLimiter.getStatus(actualKey);
3389
+ let rateLimitScore = 100;
3390
+ if (rateStatus.limit > 0) {
3391
+ const utilization = rateStatus.used / rateStatus.limit;
3392
+ rateLimitScore = Math.max(0, Math.round((1 - utilization) * 100));
3393
+ }
3394
+ // Error rate component
3395
+ const keyUsage = this.gate.meter.getKeyUsage(actualKey);
3396
+ let errorScore = 100;
3397
+ if (keyUsage.totalCalls > 0) {
3398
+ const errorRate = keyUsage.totalDenied / keyUsage.totalCalls;
3399
+ errorScore = Math.max(0, Math.round((1 - errorRate) * 100));
3400
+ }
3401
+ const healthScore = Math.round(balanceScore * 0.30 + quotaScore * 0.25 + rateLimitScore * 0.20 + errorScore * 0.25);
3402
+ let healthStatus = 'healthy';
3403
+ if (healthScore < 25)
3404
+ healthStatus = 'critical';
3405
+ else if (healthScore < 50)
3406
+ healthStatus = 'warning';
3407
+ else if (healthScore < 75)
3408
+ healthStatus = 'caution';
3409
+ else if (healthScore < 90)
3410
+ healthStatus = 'good';
3411
+ // ── Velocity ──
3412
+ const velocitySummary = {
3413
+ creditsPerHour: velocity.creditsPerHour,
3414
+ creditsPerDay: velocity.creditsPerDay,
3415
+ callsPerHour: velocity.callsPerHour,
3416
+ callsPerDay: velocity.callsPerDay,
3417
+ estimatedDepletionDate: velocity.estimatedDepletionDate || null,
3418
+ };
3419
+ // ── Rate limits ──
3420
+ const rateLimits = {
3421
+ global: {
3422
+ limit: rateStatus.limit,
3423
+ used: rateStatus.used,
3424
+ remaining: rateStatus.remaining,
3425
+ resetInMs: rateStatus.resetInMs,
3426
+ },
3427
+ };
3428
+ // ── Quotas ──
3429
+ let quotas;
3430
+ if (quotaConfig) {
3431
+ this.gate.quotaTracker.resetIfNeeded(record);
3432
+ quotas = {
3433
+ source: record.quota ? 'per-key' : 'global',
3434
+ daily: {
3435
+ callsUsed: record.quotaDailyCalls,
3436
+ callsLimit: quotaConfig.dailyCallLimit || 0,
3437
+ creditsUsed: record.quotaDailyCredits,
3438
+ creditsLimit: quotaConfig.dailyCreditLimit || 0,
3439
+ },
3440
+ monthly: {
3441
+ callsUsed: record.quotaMonthlyCalls,
3442
+ callsLimit: quotaConfig.monthlyCallLimit || 0,
3443
+ creditsUsed: record.quotaMonthlyCredits,
3444
+ creditsLimit: quotaConfig.monthlyCreditLimit || 0,
3445
+ },
3446
+ };
3447
+ }
3448
+ // ── Usage summary ──
3449
+ const usage = {
3450
+ totalCalls: keyUsage.totalCalls || 0,
3451
+ totalAllowed: keyUsage.totalAllowed || 0,
3452
+ totalDenied: keyUsage.totalDenied || 0,
3453
+ totalCredits: keyUsage.totalCreditsSpent || 0,
3454
+ };
3455
+ // ── Recent activity (last 10 events) ──
3456
+ const maskedForAudit = (0, audit_1.maskKeyForAudit)(actualKey);
3457
+ const auditResult = this.audit.query({ limit: 100 });
3458
+ const recentActivity = auditResult.events
3459
+ .filter(e => {
3460
+ for (const field of ['key', 'keyMasked', 'sourceKey', 'destKey']) {
3461
+ const val = e.metadata?.[field];
3462
+ if (val && typeof val === 'string' && val === maskedForAudit)
3463
+ return true;
3464
+ }
3465
+ if (e.actor === maskedForAudit)
3466
+ return true;
3467
+ return false;
3468
+ })
3469
+ .slice(0, 10)
3470
+ .map(e => ({
3471
+ timestamp: e.timestamp,
3472
+ event: e.type,
3473
+ ...(e.metadata?.tool ? { tool: e.metadata.tool } : {}),
3474
+ ...(e.metadata?.credits ? { credits: e.metadata.credits } : {}),
3475
+ }));
3476
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3477
+ res.end(JSON.stringify({
3478
+ ...metadata,
3479
+ balance,
3480
+ health: { score: healthScore, status: healthStatus },
3481
+ velocity: velocitySummary,
3482
+ rateLimits,
3483
+ ...(quotas ? { quotas } : {}),
3484
+ usage,
3485
+ recentActivity: recentActivity.length > 0 ? recentActivity : [],
3486
+ }));
3487
+ }
3292
3488
  // ─── /keys/auto-topup — Configure auto-topup ────────────────────────────────
3293
3489
  async handleSetAutoTopup(req, res) {
3294
3490
  if (req.method !== 'POST') {
@@ -4107,6 +4303,142 @@ class PayGateServer {
4107
4303
  }
4108
4304
  });
4109
4305
  }
4306
+ // ─── /admin/notifications — Actionable notifications ──────────────────────
4307
+ handleAdminNotifications(req, res) {
4308
+ if (!this.checkAdmin(req, res))
4309
+ return;
4310
+ const urlParts = req.url?.split('?') || [];
4311
+ const params = new URLSearchParams(urlParts[1] || '');
4312
+ const severityFilter = params.get('severity'); // critical, warning, info, or null for all
4313
+ const notifications = [];
4314
+ // Scan all keys for issues
4315
+ const allRecords = this.gate.store.getAllRecords();
4316
+ for (const record of allRecords) {
4317
+ const maskedKey = record.key.slice(0, 7) + '...' + record.key.slice(-4);
4318
+ const keyName = record.name || undefined;
4319
+ // Skip revoked keys
4320
+ if (!record.active)
4321
+ continue;
4322
+ // ── Expiry checks ──
4323
+ if (record.expiresAt) {
4324
+ const msToExpiry = new Date(record.expiresAt).getTime() - Date.now();
4325
+ const hoursToExpiry = msToExpiry / 3_600_000;
4326
+ if (msToExpiry <= 0) {
4327
+ notifications.push({
4328
+ severity: 'critical', category: 'key_expired',
4329
+ message: `Key has expired`,
4330
+ key: maskedKey, keyName,
4331
+ details: { expiresAt: record.expiresAt },
4332
+ });
4333
+ }
4334
+ else if (hoursToExpiry <= 24) {
4335
+ notifications.push({
4336
+ severity: 'critical', category: 'key_expiring_soon',
4337
+ message: `Key expires within ${Math.ceil(hoursToExpiry)} hours`,
4338
+ key: maskedKey, keyName,
4339
+ details: { expiresAt: record.expiresAt, hoursRemaining: Math.round(hoursToExpiry * 10) / 10 },
4340
+ });
4341
+ }
4342
+ else if (hoursToExpiry <= 168) {
4343
+ notifications.push({
4344
+ severity: 'warning', category: 'key_expiring_soon',
4345
+ message: `Key expires within ${Math.ceil(hoursToExpiry / 24)} days`,
4346
+ key: maskedKey, keyName,
4347
+ details: { expiresAt: record.expiresAt, daysRemaining: Math.round(hoursToExpiry / 24 * 10) / 10 },
4348
+ });
4349
+ }
4350
+ }
4351
+ // ── Credit checks ──
4352
+ if (record.credits <= 0) {
4353
+ notifications.push({
4354
+ severity: 'critical', category: 'zero_credits',
4355
+ message: `Key has zero credits remaining`,
4356
+ key: maskedKey, keyName,
4357
+ });
4358
+ }
4359
+ else {
4360
+ // Check spending velocity for depletion
4361
+ const velocity = this.creditLedger.getSpendingVelocity(record.key, record.credits, 24);
4362
+ if (velocity.creditsPerHour > 0) {
4363
+ const hoursLeft = velocity.estimatedHoursRemaining ?? Infinity;
4364
+ if (hoursLeft <= 6) {
4365
+ notifications.push({
4366
+ severity: 'critical', category: 'credits_depleting',
4367
+ message: `Credits will deplete in ~${Math.ceil(hoursLeft)} hours at current rate`,
4368
+ key: maskedKey, keyName,
4369
+ details: { credits: record.credits, creditsPerHour: velocity.creditsPerHour, estimatedHoursRemaining: Math.round(hoursLeft * 10) / 10 },
4370
+ });
4371
+ }
4372
+ else if (hoursLeft <= 24) {
4373
+ notifications.push({
4374
+ severity: 'warning', category: 'credits_depleting',
4375
+ message: `Credits will deplete in ~${Math.ceil(hoursLeft)} hours at current rate`,
4376
+ key: maskedKey, keyName,
4377
+ details: { credits: record.credits, creditsPerHour: velocity.creditsPerHour, estimatedHoursRemaining: Math.round(hoursLeft * 10) / 10 },
4378
+ });
4379
+ }
4380
+ }
4381
+ }
4382
+ // ── Suspended key ──
4383
+ if (record.suspended) {
4384
+ notifications.push({
4385
+ severity: 'info', category: 'key_suspended',
4386
+ message: `Key is suspended`,
4387
+ key: maskedKey, keyName,
4388
+ });
4389
+ }
4390
+ // ── Error rate check ──
4391
+ const keyUsage = this.gate.meter.getKeyUsage(record.key);
4392
+ if (keyUsage.totalCalls >= 10) {
4393
+ const errorRate = keyUsage.totalDenied / keyUsage.totalCalls;
4394
+ if (errorRate >= 0.5) {
4395
+ notifications.push({
4396
+ severity: 'critical', category: 'high_error_rate',
4397
+ message: `${Math.round(errorRate * 100)}% error rate (${keyUsage.totalDenied}/${keyUsage.totalCalls} denied)`,
4398
+ key: maskedKey, keyName,
4399
+ details: { errorRate: Math.round(errorRate * 1000) / 1000, totalCalls: keyUsage.totalCalls, totalDenied: keyUsage.totalDenied },
4400
+ });
4401
+ }
4402
+ else if (errorRate >= 0.25) {
4403
+ notifications.push({
4404
+ severity: 'warning', category: 'high_error_rate',
4405
+ message: `${Math.round(errorRate * 100)}% error rate (${keyUsage.totalDenied}/${keyUsage.totalCalls} denied)`,
4406
+ key: maskedKey, keyName,
4407
+ details: { errorRate: Math.round(errorRate * 1000) / 1000, totalCalls: keyUsage.totalCalls, totalDenied: keyUsage.totalDenied },
4408
+ });
4409
+ }
4410
+ }
4411
+ // ── Rate limit pressure ──
4412
+ const rateStatus = this.gate.rateLimiter.getStatus(record.key);
4413
+ if (rateStatus.limit > 0) {
4414
+ const utilization = rateStatus.used / rateStatus.limit;
4415
+ if (utilization >= 0.9) {
4416
+ notifications.push({
4417
+ severity: 'warning', category: 'rate_limit_pressure',
4418
+ message: `${Math.round(utilization * 100)}% rate limit utilization (${rateStatus.used}/${rateStatus.limit})`,
4419
+ key: maskedKey, keyName,
4420
+ details: { used: rateStatus.used, limit: rateStatus.limit, remaining: rateStatus.remaining },
4421
+ });
4422
+ }
4423
+ }
4424
+ }
4425
+ // Apply severity filter
4426
+ let filtered = notifications;
4427
+ if (severityFilter) {
4428
+ filtered = notifications.filter(n => n.severity === severityFilter);
4429
+ }
4430
+ // Sort: critical first, then warning, then info
4431
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
4432
+ filtered.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
4433
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4434
+ res.end(JSON.stringify({
4435
+ total: filtered.length,
4436
+ critical: filtered.filter(n => n.severity === 'critical').length,
4437
+ warning: filtered.filter(n => n.severity === 'warning').length,
4438
+ info: filtered.filter(n => n.severity === 'info').length,
4439
+ notifications: filtered,
4440
+ }));
4441
+ }
4110
4442
  // ─── /keys/notes — Timestamped notes on API keys ─────────────────────────
4111
4443
  handleGetNotes(req, res) {
4112
4444
  if (!this.checkAdmin(req, res))