paygate-mcp 1.7.0 → 1.9.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
@@ -50,6 +50,9 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
50
50
  - **Admin Lifecycle Events** — Webhook notifications for key.created, key.revoked, key.rotated, key.topup
51
51
  - **IP Allowlisting** — Restrict API keys to specific IPs or CIDR ranges (IPv4)
52
52
  - **Key Tags/Metadata** — Attach arbitrary key-value tags to API keys for external system integration
53
+ - **Usage Analytics** — Time-series analytics API with tool breakdown, top consumers, and trend comparison
54
+ - **Alert Webhooks** — Configurable alerts for spending thresholds, low credits, quota warnings, key expiry, rate limit spikes
55
+ - **Team Management** — Group API keys into teams with shared budgets, quotas, and usage tracking
53
56
  - **Refund on Failure** — Automatically refund credits when downstream tool calls fail
54
57
  - **Webhook Events** — POST batched usage events to any URL for external billing/alerting
55
58
  - **Config File Mode** — Load all settings from a JSON file (`--config`)
@@ -286,6 +289,16 @@ A real-time admin UI for managing keys, viewing usage, and monitoring tool calls
286
289
  | `/.well-known/mcp-payment` | GET | None | Server payment metadata (SEP-2007) |
287
290
  | `/pricing` | GET | None | Full per-tool pricing breakdown |
288
291
  | `/metrics` | GET | None | Prometheus metrics (counters, gauges, uptime) |
292
+ | `/analytics` | GET | `X-Admin-Key` | Usage analytics (time-series, tool breakdown, trends) |
293
+ | `/alerts` | GET | `X-Admin-Key` | Consume pending alerts |
294
+ | `/alerts` | POST | `X-Admin-Key` | Configure alert rules |
295
+ | `/teams` | GET | `X-Admin-Key` | List all teams |
296
+ | `/teams` | POST | `X-Admin-Key` | Create a team (name, budget, quota, tags) |
297
+ | `/teams/update` | POST | `X-Admin-Key` | Update team settings |
298
+ | `/teams/delete` | POST | `X-Admin-Key` | Delete (deactivate) a team |
299
+ | `/teams/assign` | POST | `X-Admin-Key` | Assign an API key to a team |
300
+ | `/teams/remove` | POST | `X-Admin-Key` | Remove an API key from a team |
301
+ | `/teams/usage` | GET | `X-Admin-Key` | Team usage summary with member breakdown |
289
302
  | `/audit` | GET | `X-Admin-Key` | Query audit log (filter by type, actor, time) |
290
303
  | `/audit/export` | GET | `X-Admin-Key` | Export full audit log (JSON or CSV) |
291
304
  | `/audit/stats` | GET | `X-Admin-Key` | Audit log statistics |
@@ -510,6 +523,13 @@ When webhooks are enabled, admin operations also fire webhook events:
510
523
  | `key.topup` | POST /topup | keyMasked, creditsAdded, newBalance |
511
524
  | `key.revoked` | POST /keys/revoke | keyMasked |
512
525
  | `key.rotated` | POST /keys/rotate | oldKeyMasked, newKeyMasked |
526
+ | `key.expired` | Gate evaluation | keyMasked |
527
+ | `alert.fired` | Gate evaluation | alertType, keyPrefix, message, value, threshold |
528
+ | `team.created` | POST /teams | teamId, name, budget |
529
+ | `team.updated` | POST /teams/update | teamId, changes |
530
+ | `team.deleted` | POST /teams/delete | teamId |
531
+ | `team.key_assigned` | POST /teams/assign | teamId, keyMasked |
532
+ | `team.key_removed` | POST /teams/remove | teamId, keyMasked |
513
533
 
514
534
  Admin events appear in the `adminEvents` array of the webhook payload (separate from usage `events`). Both arrays can be present in the same batch.
515
535
 
@@ -649,7 +669,7 @@ curl http://localhost:3402/audit/stats \
649
669
  -H "X-Admin-Key: YOUR_ADMIN_KEY"
650
670
  ```
651
671
 
652
- **Tracked events:** `key.created`, `key.revoked`, `key.topup`, `key.acl_updated`, `key.expiry_updated`, `key.quota_updated`, `key.limit_updated`, `gate.allow`, `gate.deny`, `session.created`, `session.destroyed`, `oauth.client_registered`, `oauth.token_issued`, `oauth.token_revoked`, `admin.auth_failed`, `billing.refund`.
672
+ **Tracked events:** `key.created`, `key.revoked`, `key.topup`, `key.acl_updated`, `key.expiry_updated`, `key.quota_updated`, `key.limit_updated`, `key.tags_updated`, `key.ip_updated`, `gate.allow`, `gate.deny`, `session.created`, `session.destroyed`, `oauth.client_registered`, `oauth.token_issued`, `oauth.token_revoked`, `admin.auth_failed`, `admin.alerts_configured`, `billing.refund`, `team.created`, `team.updated`, `team.deleted`, `team.key_assigned`, `team.key_removed`.
653
673
 
654
674
  **Retention:** Ring buffer (default 10,000 events), age-based cleanup (default 30 days), automatic periodic enforcement.
655
675
 
@@ -789,6 +809,107 @@ curl -X POST http://localhost:3402/keys \
789
809
 
790
810
  Limits: max 50 tags per key, max 100 chars per key/value. Tags appear in `/balance` responses and key listings.
791
811
 
812
+ ### Usage Analytics
813
+
814
+ Query aggregated usage data for dashboards, reports, and trend analysis:
815
+
816
+ ```bash
817
+ # Get analytics for the last 24 hours (hourly buckets)
818
+ curl "http://localhost:3402/analytics?from=2026-02-25T00:00:00Z&to=2026-02-26T00:00:00Z&granularity=hourly" \
819
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
820
+
821
+ # Daily granularity with top 5 consumers
822
+ curl "http://localhost:3402/analytics?granularity=daily&topN=5" \
823
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
824
+ ```
825
+
826
+ Returns:
827
+ - **timeSeries** — Bucketed call counts, credits charged, and denials per time window
828
+ - **toolBreakdown** — Per-tool stats (calls, credits, average cost) sorted by usage
829
+ - **topConsumers** — Top N API keys by credits spent, with each key's most-used tool
830
+ - **trend** — Current vs previous period comparison with percentage changes (calls, credits, denials)
831
+ - **summary** — Total calls, credits, unique keys, and unique tools
832
+
833
+ Query parameters: `from` (ISO date), `to` (ISO date), `granularity` (`hourly` or `daily`, default: `hourly`), `topN` (number, default: `10`).
834
+
835
+ ### Alert Webhooks
836
+
837
+ Configure rules to fire alerts when usage thresholds are crossed. Alerts are delivered via webhooks as `alert.fired` admin events:
838
+
839
+ ```bash
840
+ # Configure alert rules
841
+ curl -X POST http://localhost:3402/alerts \
842
+ -H "Content-Type: application/json" \
843
+ -H "X-Admin-Key: YOUR_ADMIN_KEY" \
844
+ -d '{"rules": [
845
+ {"type": "spending_threshold", "threshold": 80},
846
+ {"type": "credits_low", "threshold": 50},
847
+ {"type": "quota_warning", "threshold": 90},
848
+ {"type": "key_expiry_soon", "threshold": 86400},
849
+ {"type": "rate_limit_spike", "threshold": 10}
850
+ ]}'
851
+
852
+ # Consume pending alerts (returns and clears queue)
853
+ curl http://localhost:3402/alerts \
854
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
855
+ ```
856
+
857
+ **Alert types:**
858
+
859
+ | Type | Threshold Meaning | Fires When |
860
+ |------|-------------------|------------|
861
+ | `spending_threshold` | Percentage (0–100) | Key has spent ≥ threshold% of its initial credits |
862
+ | `credits_low` | Absolute credits | Key's remaining credits drop below threshold |
863
+ | `quota_warning` | Percentage (0–100) | Key's daily call usage exceeds threshold% of quota |
864
+ | `key_expiry_soon` | Seconds | Key expires within threshold seconds |
865
+ | `rate_limit_spike` | Count | Key has ≥ threshold rate-limit denials in 5 minutes |
866
+
867
+ Each rule has an optional `cooldownSeconds` (default: 300) to prevent alert storms. Alerts are automatically checked on every gate evaluation (tool call).
868
+
869
+ When webhooks are enabled (`--webhook-url`), alerts fire as `alert.fired` events in the `adminEvents` webhook payload with full context (key, rule type, current value, threshold).
870
+
871
+ ### Team Management
872
+
873
+ Group API keys into teams with shared budgets, quotas, and usage tracking. Teams enforce budget and quota limits at the gate level — if a key belongs to a team that has exceeded its budget or quota, tool calls are denied even if the individual key has credits remaining.
874
+
875
+ ```bash
876
+ # Create a team
877
+ curl -X POST http://localhost:3402/teams \
878
+ -H "Content-Type: application/json" \
879
+ -H "X-Admin-Key: YOUR_ADMIN_KEY" \
880
+ -d '{"name": "Engineering", "budget": 10000, "tags": {"dept": "eng"}}'
881
+
882
+ # Assign an API key to a team
883
+ curl -X POST http://localhost:3402/teams/assign \
884
+ -H "Content-Type: application/json" \
885
+ -H "X-Admin-Key: YOUR_ADMIN_KEY" \
886
+ -d '{"teamId": "team_abc123...", "apiKey": "pg_abc123..."}'
887
+
888
+ # Set team quotas (daily/monthly limits)
889
+ curl -X POST http://localhost:3402/teams/update \
890
+ -H "Content-Type: application/json" \
891
+ -H "X-Admin-Key: YOUR_ADMIN_KEY" \
892
+ -d '{"teamId": "team_abc123...", "quota": {"dailyCalls": 1000, "monthlyCalls": 25000, "dailyCredits": 5000, "monthlyCredits": 100000}}'
893
+
894
+ # View team usage with member breakdown
895
+ curl "http://localhost:3402/teams/usage?teamId=team_abc123..." \
896
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
897
+ ```
898
+
899
+ **Team features:**
900
+
901
+ | Feature | Description |
902
+ |---------|-------------|
903
+ | Shared budget | Pool credits across all team members (0 = unlimited) |
904
+ | Team quotas | Daily/monthly call and credit limits (UTC auto-reset) |
905
+ | Member breakdown | Per-key usage within the team (keys masked) |
906
+ | Tags/metadata | Attach key-value pairs for department, project, etc. |
907
+ | Max 100 keys | Per team limit to prevent abuse |
908
+ | Gate integration | Budget + quota checked on every tool call |
909
+ | Audit trail | team.created, team.updated, team.deleted, team.key_assigned, team.key_removed events |
910
+
911
+ Each API key can belong to at most one team. Team budget and quota checks happen after individual key checks — both must pass for a tool call to succeed.
912
+
792
913
  ### Rate Limit Response Headers
793
914
 
794
915
  Every `/mcp` response includes rate limit and credits headers when an API key is provided:
@@ -904,6 +1025,9 @@ const result = await client.callTool('search', { query: 'hello' });
904
1025
  - Audit log with retention policies (ring buffer, age-based cleanup)
905
1026
  - API keys masked in audit events (only first 7 + last 4 chars visible)
906
1027
  - Discovery endpoints (/.well-known/mcp-payment, /pricing) are public but read-only
1028
+ - Team budgets enforce integer arithmetic (no float bypass)
1029
+ - Keys masked in team usage summaries (first 7 + last 4 chars only)
1030
+ - Team quota resets atomic at UTC day/month boundaries
907
1031
  - Red-teamed with 101 adversarial security tests across 14 passes
908
1032
 
909
1033
  ## Current Limitations
@@ -941,6 +1065,9 @@ const result = await client.callTool('search', { query: 'hello' });
941
1065
  - [x] Admin lifecycle events — Webhook notifications for key management operations
942
1066
  - [x] IP allowlisting — Restrict API keys to specific IPs or CIDR ranges
943
1067
  - [x] Key tags/metadata — Attach key-value tags for external system integration
1068
+ - [x] Usage analytics — Time-series analytics API with tool breakdown, trends, and top consumers
1069
+ - [x] Alert webhooks — Configurable threshold alerts (spending, credits, quota, expiry, rate limits)
1070
+ - [x] Team management — Group API keys with shared budgets, quotas, and usage tracking
944
1071
  - [ ] Horizontal scaling — Redis-backed state for multi-process deployments
945
1072
 
946
1073
  ## Requirements
@@ -0,0 +1,85 @@
1
+ /**
2
+ * AlertEngine — Configurable alerts for proactive monitoring.
3
+ *
4
+ * Alert types:
5
+ * - spending_threshold: Fires when a key's totalSpent crosses X% of its credits
6
+ * - credits_low: Fires when remaining credits fall below threshold
7
+ * - quota_warning: Fires when daily/monthly quota usage exceeds X%
8
+ * - key_expiry_soon: Fires when a key will expire within N seconds
9
+ * - rate_limit_spike: Fires when rate limit denials exceed N in a window
10
+ *
11
+ * Each alert fires at most once per key per cooldown period (default 1 hour).
12
+ */
13
+ import { ApiKeyRecord } from './types';
14
+ export type AlertType = 'spending_threshold' | 'credits_low' | 'quota_warning' | 'key_expiry_soon' | 'rate_limit_spike';
15
+ export interface AlertRule {
16
+ type: AlertType;
17
+ /** Threshold value — meaning depends on type:
18
+ * - spending_threshold: percentage (0-100) of initial credits spent
19
+ * - credits_low: absolute credits remaining
20
+ * - quota_warning: percentage (0-100) of quota used
21
+ * - key_expiry_soon: seconds before expiry to alert
22
+ * - rate_limit_spike: number of denials in a 5-min window
23
+ */
24
+ threshold: number;
25
+ /** Cooldown in seconds between repeated alerts for same key (default: 3600) */
26
+ cooldownSeconds?: number;
27
+ }
28
+ export interface Alert {
29
+ type: AlertType;
30
+ timestamp: string;
31
+ keyPrefix: string;
32
+ keyName: string;
33
+ message: string;
34
+ threshold: number;
35
+ currentValue: number;
36
+ metadata: Record<string, unknown>;
37
+ }
38
+ export interface AlertEngineConfig {
39
+ rules: AlertRule[];
40
+ /** If true, alerts are only logged but not emitted */
41
+ dryRun?: boolean;
42
+ }
43
+ export declare class AlertEngine {
44
+ private readonly rules;
45
+ private readonly dryRun;
46
+ /** Map of "alertType:keyPrefix" → last alert timestamp */
47
+ private readonly cooldowns;
48
+ /** Rate limit denial counter: "keyPrefix" → timestamps of recent denials */
49
+ private readonly rateLimitDenials;
50
+ /** Pending alerts waiting to be consumed */
51
+ private pendingAlerts;
52
+ constructor(config?: AlertEngineConfig);
53
+ /**
54
+ * Check a key record against all alert rules.
55
+ * Call this after every gate evaluation.
56
+ */
57
+ check(key: string, record: ApiKeyRecord, context?: {
58
+ rateLimitDenied?: boolean;
59
+ }): Alert[];
60
+ /**
61
+ * Record a rate limit denial for spike detection.
62
+ */
63
+ recordRateLimitDenial(key: string): void;
64
+ /**
65
+ * Consume pending alerts (returns and clears the queue).
66
+ */
67
+ consumeAlerts(): Alert[];
68
+ /**
69
+ * Get pending alert count.
70
+ */
71
+ get pendingCount(): number;
72
+ /**
73
+ * Get configured rules.
74
+ */
75
+ get configuredRules(): AlertRule[];
76
+ /**
77
+ * Evaluate a single rule against a key record.
78
+ */
79
+ private evaluateRule;
80
+ /**
81
+ * Clear cooldowns (for testing).
82
+ */
83
+ clearCooldowns(): void;
84
+ }
85
+ //# sourceMappingURL=alerts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alerts.d.ts","sourceRoot":"","sources":["../src/alerts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIvC,MAAM,MAAM,SAAS,GACjB,oBAAoB,GACpB,aAAa,GACb,eAAe,GACf,iBAAiB,GACjB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,sDAAsD;IACtD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAID,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+B;IAChE,4CAA4C;IAC5C,OAAO,CAAC,aAAa,CAAe;gBAExB,MAAM,CAAC,EAAE,iBAAiB;IAKtC;;;OAGG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,KAAK,EAAE;IAyB1F;;OAEG;IACH,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAcxC;;OAEG;IACH,aAAa,IAAI,KAAK,EAAE;IAMxB;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;OAEG;IACH,IAAI,eAAe,IAAI,SAAS,EAAE,CAEjC;IAED;;OAEG;IACH,OAAO,CAAC,YAAY;IAuHpB;;OAEG;IACH,cAAc,IAAI,IAAI;CAKvB"}
package/dist/alerts.js ADDED
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * AlertEngine — Configurable alerts for proactive monitoring.
4
+ *
5
+ * Alert types:
6
+ * - spending_threshold: Fires when a key's totalSpent crosses X% of its credits
7
+ * - credits_low: Fires when remaining credits fall below threshold
8
+ * - quota_warning: Fires when daily/monthly quota usage exceeds X%
9
+ * - key_expiry_soon: Fires when a key will expire within N seconds
10
+ * - rate_limit_spike: Fires when rate limit denials exceed N in a window
11
+ *
12
+ * Each alert fires at most once per key per cooldown period (default 1 hour).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.AlertEngine = void 0;
16
+ // ─── Engine ───────────────────────────────────────────────────────────────────
17
+ class AlertEngine {
18
+ rules;
19
+ dryRun;
20
+ /** Map of "alertType:keyPrefix" → last alert timestamp */
21
+ cooldowns = new Map();
22
+ /** Rate limit denial counter: "keyPrefix" → timestamps of recent denials */
23
+ rateLimitDenials = new Map();
24
+ /** Pending alerts waiting to be consumed */
25
+ pendingAlerts = [];
26
+ constructor(config) {
27
+ this.rules = config?.rules || [];
28
+ this.dryRun = config?.dryRun || false;
29
+ }
30
+ /**
31
+ * Check a key record against all alert rules.
32
+ * Call this after every gate evaluation.
33
+ */
34
+ check(key, record, context) {
35
+ const fired = [];
36
+ const keyPrefix = key.slice(0, 10) + '...';
37
+ for (const rule of this.rules) {
38
+ const cooldownKey = `${rule.type}:${keyPrefix}`;
39
+ const cooldownMs = (rule.cooldownSeconds || 3600) * 1000;
40
+ const lastFired = this.cooldowns.get(cooldownKey) || 0;
41
+ const now = Date.now();
42
+ if (now - lastFired < cooldownMs)
43
+ continue;
44
+ const alert = this.evaluateRule(rule, key, record, keyPrefix, context);
45
+ if (alert) {
46
+ this.cooldowns.set(cooldownKey, now);
47
+ fired.push(alert);
48
+ if (!this.dryRun) {
49
+ this.pendingAlerts.push(alert);
50
+ }
51
+ }
52
+ }
53
+ return fired;
54
+ }
55
+ /**
56
+ * Record a rate limit denial for spike detection.
57
+ */
58
+ recordRateLimitDenial(key) {
59
+ const keyPrefix = key.slice(0, 10) + '...';
60
+ let denials = this.rateLimitDenials.get(keyPrefix);
61
+ if (!denials) {
62
+ denials = [];
63
+ this.rateLimitDenials.set(keyPrefix, denials);
64
+ }
65
+ denials.push(Date.now());
66
+ // Keep only last 5 minutes
67
+ const cutoff = Date.now() - 5 * 60 * 1000;
68
+ this.rateLimitDenials.set(keyPrefix, denials.filter(t => t > cutoff));
69
+ }
70
+ /**
71
+ * Consume pending alerts (returns and clears the queue).
72
+ */
73
+ consumeAlerts() {
74
+ const alerts = [...this.pendingAlerts];
75
+ this.pendingAlerts = [];
76
+ return alerts;
77
+ }
78
+ /**
79
+ * Get pending alert count.
80
+ */
81
+ get pendingCount() {
82
+ return this.pendingAlerts.length;
83
+ }
84
+ /**
85
+ * Get configured rules.
86
+ */
87
+ get configuredRules() {
88
+ return [...this.rules];
89
+ }
90
+ /**
91
+ * Evaluate a single rule against a key record.
92
+ */
93
+ evaluateRule(rule, key, record, keyPrefix, context) {
94
+ const now = new Date().toISOString();
95
+ const base = {
96
+ type: rule.type,
97
+ timestamp: now,
98
+ keyPrefix,
99
+ keyName: record.name,
100
+ threshold: rule.threshold,
101
+ };
102
+ switch (rule.type) {
103
+ case 'spending_threshold': {
104
+ // Alert when totalSpent exceeds threshold% of (totalSpent + remaining credits)
105
+ const totalBudget = record.credits + record.totalSpent;
106
+ if (totalBudget <= 0)
107
+ return null;
108
+ const spentPct = (record.totalSpent / totalBudget) * 100;
109
+ if (spentPct >= rule.threshold) {
110
+ return {
111
+ ...base,
112
+ currentValue: Math.round(spentPct * 10) / 10,
113
+ message: `Key "${record.name}" has spent ${Math.round(spentPct)}% of total budget (${record.totalSpent}/${totalBudget} credits)`,
114
+ metadata: { credits: record.credits, totalSpent: record.totalSpent, totalBudget },
115
+ };
116
+ }
117
+ return null;
118
+ }
119
+ case 'credits_low': {
120
+ if (record.credits <= rule.threshold) {
121
+ return {
122
+ ...base,
123
+ currentValue: record.credits,
124
+ message: `Key "${record.name}" has only ${record.credits} credits remaining (threshold: ${rule.threshold})`,
125
+ metadata: { credits: record.credits },
126
+ };
127
+ }
128
+ return null;
129
+ }
130
+ case 'quota_warning': {
131
+ if (!record.quota)
132
+ return null;
133
+ // Check daily call quota
134
+ if (record.quota.dailyCallLimit > 0) {
135
+ const pct = (record.quotaDailyCalls / record.quota.dailyCallLimit) * 100;
136
+ if (pct >= rule.threshold) {
137
+ return {
138
+ ...base,
139
+ currentValue: Math.round(pct * 10) / 10,
140
+ message: `Key "${record.name}" daily call quota at ${Math.round(pct)}% (${record.quotaDailyCalls}/${record.quota.dailyCallLimit})`,
141
+ metadata: {
142
+ quotaType: 'dailyCalls',
143
+ used: record.quotaDailyCalls,
144
+ limit: record.quota.dailyCallLimit,
145
+ },
146
+ };
147
+ }
148
+ }
149
+ // Check monthly call quota
150
+ if (record.quota.monthlyCallLimit > 0) {
151
+ const pct = (record.quotaMonthlyCalls / record.quota.monthlyCallLimit) * 100;
152
+ if (pct >= rule.threshold) {
153
+ return {
154
+ ...base,
155
+ currentValue: Math.round(pct * 10) / 10,
156
+ message: `Key "${record.name}" monthly call quota at ${Math.round(pct)}% (${record.quotaMonthlyCalls}/${record.quota.monthlyCallLimit})`,
157
+ metadata: {
158
+ quotaType: 'monthlyCalls',
159
+ used: record.quotaMonthlyCalls,
160
+ limit: record.quota.monthlyCallLimit,
161
+ },
162
+ };
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+ case 'key_expiry_soon': {
168
+ if (!record.expiresAt)
169
+ return null;
170
+ const expiresMs = new Date(record.expiresAt).getTime();
171
+ const remainingMs = expiresMs - Date.now();
172
+ const thresholdMs = rule.threshold * 1000;
173
+ if (remainingMs > 0 && remainingMs <= thresholdMs) {
174
+ const remainingHours = Math.round(remainingMs / 3600_000 * 10) / 10;
175
+ return {
176
+ ...base,
177
+ currentValue: Math.round(remainingMs / 1000),
178
+ message: `Key "${record.name}" expires in ${remainingHours} hours (${record.expiresAt})`,
179
+ metadata: { expiresAt: record.expiresAt, remainingSeconds: Math.round(remainingMs / 1000) },
180
+ };
181
+ }
182
+ return null;
183
+ }
184
+ case 'rate_limit_spike': {
185
+ if (!context?.rateLimitDenied)
186
+ return null;
187
+ const denials = this.rateLimitDenials.get(keyPrefix) || [];
188
+ const recentCount = denials.length;
189
+ if (recentCount >= rule.threshold) {
190
+ return {
191
+ ...base,
192
+ currentValue: recentCount,
193
+ message: `Key "${record.name}" hit ${recentCount} rate limit denials in 5 min (threshold: ${rule.threshold})`,
194
+ metadata: { denialCount: recentCount, windowMinutes: 5 },
195
+ };
196
+ }
197
+ return null;
198
+ }
199
+ default:
200
+ return null;
201
+ }
202
+ }
203
+ /**
204
+ * Clear cooldowns (for testing).
205
+ */
206
+ clearCooldowns() {
207
+ this.cooldowns.clear();
208
+ this.rateLimitDenials.clear();
209
+ this.pendingAlerts = [];
210
+ }
211
+ }
212
+ exports.AlertEngine = AlertEngine;
213
+ //# sourceMappingURL=alerts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alerts.js","sourceRoot":"","sources":["../src/alerts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * AnalyticsEngine — Time-series analytics over UsageEvents.
3
+ *
4
+ * Provides:
5
+ * - Time-bucketed aggregation (hourly, daily)
6
+ * - Per-tool breakdown with success/failure rates
7
+ * - Top consumers by credits or call count
8
+ * - Trend comparison (current vs previous period)
9
+ * - Revenue (credits) over time
10
+ */
11
+ import { UsageEvent } from './types';
12
+ export type BucketGranularity = 'hourly' | 'daily';
13
+ export interface TimeBucket {
14
+ /** Bucket start time (ISO 8601) */
15
+ start: string;
16
+ /** Bucket end time (ISO 8601) */
17
+ end: string;
18
+ /** Total calls in this bucket */
19
+ calls: number;
20
+ /** Allowed calls */
21
+ allowed: number;
22
+ /** Denied calls */
23
+ denied: number;
24
+ /** Credits charged */
25
+ credits: number;
26
+ }
27
+ export interface ToolBreakdown {
28
+ tool: string;
29
+ calls: number;
30
+ allowed: number;
31
+ denied: number;
32
+ credits: number;
33
+ /** Success rate (0-1) */
34
+ successRate: number;
35
+ /** Average credits per allowed call */
36
+ avgCreditsPerCall: number;
37
+ }
38
+ export interface TopConsumer {
39
+ /** Key name (or masked prefix) */
40
+ keyName: string;
41
+ calls: number;
42
+ credits: number;
43
+ denied: number;
44
+ /** Most-used tool */
45
+ topTool: string;
46
+ }
47
+ export interface TrendComparison {
48
+ /** Current period stats */
49
+ current: {
50
+ calls: number;
51
+ credits: number;
52
+ denied: number;
53
+ };
54
+ /** Previous period stats (same duration) */
55
+ previous: {
56
+ calls: number;
57
+ credits: number;
58
+ denied: number;
59
+ };
60
+ /** Percentage change (-100 to +Infinity) */
61
+ change: {
62
+ calls: number | null;
63
+ credits: number | null;
64
+ denied: number | null;
65
+ };
66
+ }
67
+ export interface AnalyticsReport {
68
+ /** Query time range */
69
+ from: string;
70
+ to: string;
71
+ granularity: BucketGranularity;
72
+ /** Time-series data */
73
+ timeSeries: TimeBucket[];
74
+ /** Per-tool breakdown (sorted by calls desc) */
75
+ tools: ToolBreakdown[];
76
+ /** Top consumers (sorted by credits desc) */
77
+ topConsumers: TopConsumer[];
78
+ /** Trend vs previous equivalent period */
79
+ trend: TrendComparison;
80
+ /** Summary stats */
81
+ summary: {
82
+ totalCalls: number;
83
+ totalCredits: number;
84
+ totalDenied: number;
85
+ uniqueKeys: number;
86
+ uniqueTools: number;
87
+ successRate: number;
88
+ };
89
+ }
90
+ export declare class AnalyticsEngine {
91
+ /**
92
+ * Generate a full analytics report from usage events.
93
+ */
94
+ report(events: UsageEvent[], options?: {
95
+ from?: string;
96
+ to?: string;
97
+ granularity?: BucketGranularity;
98
+ topN?: number;
99
+ }): AnalyticsReport;
100
+ /**
101
+ * Bucket events into time intervals.
102
+ */
103
+ private buildTimeSeries;
104
+ /**
105
+ * Breakdown per tool, sorted by calls descending.
106
+ */
107
+ private buildToolBreakdown;
108
+ /**
109
+ * Top consumers by credits spent, with their most-used tool.
110
+ */
111
+ private buildTopConsumers;
112
+ /**
113
+ * Compare current period vs previous period of same length.
114
+ */
115
+ private buildTrend;
116
+ }
117
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}