paygate-mcp 1.8.0 → 2.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/server.js CHANGED
@@ -41,6 +41,9 @@ const metrics_1 = require("./metrics");
41
41
  const dashboard_1 = require("./dashboard");
42
42
  const analytics_1 = require("./analytics");
43
43
  const alerts_1 = require("./alerts");
44
+ const teams_1 = require("./teams");
45
+ const redis_client_1 = require("./redis-client");
46
+ const redis_sync_1 = require("./redis-sync");
44
47
  /** Max request body size: 1MB */
45
48
  const MAX_BODY_SIZE = 1_048_576;
46
49
  class PayGateServer {
@@ -67,11 +70,15 @@ class PayGateServer {
67
70
  analytics;
68
71
  /** Alert engine for proactive monitoring */
69
72
  alerts;
73
+ /** Team/organization manager */
74
+ teams;
75
+ /** Redis sync adapter for distributed state (null if not using Redis) */
76
+ redisSync = null;
70
77
  /** The active request handler — either proxy or router */
71
78
  get handler() {
72
79
  return (this.router || this.proxy);
73
80
  }
74
- constructor(config, adminKey, statePath, remoteUrl, stripeWebhookSecret, servers) {
81
+ constructor(config, adminKey, statePath, remoteUrl, stripeWebhookSecret, servers, redisUrl) {
75
82
  this.config = { ...types_1.DEFAULT_CONFIG, ...config };
76
83
  this.adminKey = adminKey || `admin_${require('crypto').randomBytes(16).toString('hex')}`;
77
84
  this.gate = new gate_1.Gate(this.config, statePath);
@@ -129,8 +136,34 @@ class PayGateServer {
129
136
  this.metrics.registerGauge('paygate_pending_alerts_total', 'Number of pending alerts', () => {
130
137
  return this.alerts.pendingCount;
131
138
  });
139
+ // Team manager
140
+ this.teams = new teams_1.TeamManager();
141
+ this.metrics.registerGauge('paygate_active_teams_total', 'Number of active teams', () => {
142
+ return this.teams.listTeams().length;
143
+ });
144
+ // Wire team budget/quota checks into the gate
145
+ this.gate.teamChecker = (apiKey, credits) => {
146
+ const budgetCheck = this.teams.checkBudget(apiKey, credits);
147
+ if (!budgetCheck.allowed)
148
+ return budgetCheck;
149
+ return this.teams.checkQuota(apiKey, credits);
150
+ };
151
+ this.gate.teamRecorder = (apiKey, credits) => {
152
+ this.teams.recordUsage(apiKey, credits);
153
+ };
154
+ // Redis distributed state (if configured)
155
+ if (redisUrl) {
156
+ const redisOpts = (0, redis_client_1.parseRedisUrl)(redisUrl);
157
+ const redisClient = new redis_client_1.RedisClient(redisOpts);
158
+ this.redisSync = new redis_sync_1.RedisSync(redisClient, this.gate.store);
159
+ }
132
160
  }
133
161
  async start() {
162
+ // Initialize Redis sync before starting (loads state from Redis)
163
+ if (this.redisSync) {
164
+ await this.redisSync.init();
165
+ console.log('[paygate] Redis distributed state enabled');
166
+ }
134
167
  await this.handler.start();
135
168
  return new Promise((resolve, reject) => {
136
169
  this.server = (0, http_1.createServer)(async (req, res) => {
@@ -222,6 +255,23 @@ class PayGateServer {
222
255
  if (req.method === 'POST')
223
256
  return this.handleConfigureAlerts(req, res);
224
257
  break;
258
+ // ─── Team management endpoints ────────────────────────────────────
259
+ case '/teams':
260
+ if (req.method === 'GET')
261
+ return this.handleListTeams(req, res);
262
+ if (req.method === 'POST')
263
+ return this.handleCreateTeam(req, res);
264
+ break;
265
+ case '/teams/update':
266
+ return this.handleUpdateTeam(req, res);
267
+ case '/teams/delete':
268
+ return this.handleDeleteTeam(req, res);
269
+ case '/teams/assign':
270
+ return this.handleTeamAssignKey(req, res);
271
+ case '/teams/remove':
272
+ return this.handleTeamRemoveKey(req, res);
273
+ case '/teams/usage':
274
+ return this.handleTeamUsage(req, res);
225
275
  // ─── OAuth 2.1 endpoints ─────────────────────────────────────────
226
276
  case '/.well-known/oauth-authorization-server':
227
277
  return this.handleOAuthMetadata(req, res);
@@ -515,6 +565,12 @@ class PayGateServer {
515
565
  metrics: 'GET /metrics — Prometheus metrics (public)',
516
566
  analytics: 'GET /analytics — Usage analytics with time-series data (requires X-Admin-Key)',
517
567
  alerts: 'GET /alerts — Get pending alerts + POST /alerts — Configure alert rules (requires X-Admin-Key)',
568
+ teams: 'GET /teams — List teams + POST /teams — Create team (requires X-Admin-Key)',
569
+ teamsUpdate: 'POST /teams/update — Update team (requires X-Admin-Key)',
570
+ teamsDelete: 'POST /teams/delete — Delete team (requires X-Admin-Key)',
571
+ teamsAssign: 'POST /teams/assign — Assign key to team (requires X-Admin-Key)',
572
+ teamsRemove: 'POST /teams/remove — Remove key from team (requires X-Admin-Key)',
573
+ teamsUsage: 'GET /teams/usage?teamId=... — Team usage summary (requires X-Admin-Key)',
518
574
  ...(this.oauth ? {
519
575
  oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
520
576
  oauthRegister: 'POST /oauth/register — Register OAuth client',
@@ -526,6 +582,7 @@ class PayGateServer {
526
582
  },
527
583
  shadowMode: this.config.shadowMode,
528
584
  oauth: !!this.oauth,
585
+ redis: !!this.redisSync,
529
586
  }));
530
587
  }
531
588
  // ─── /status — Dashboard ────────────────────────────────────────────────────
@@ -1629,6 +1686,250 @@ class PayGateServer {
1629
1686
  }
1630
1687
  return true;
1631
1688
  }
1689
+ // ─── /teams — Team management ────────────────────────────────────────────
1690
+ handleListTeams(req, res) {
1691
+ if (!this.checkAdmin(req, res))
1692
+ return;
1693
+ if (req.method !== 'GET') {
1694
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1695
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
1696
+ return;
1697
+ }
1698
+ const teams = this.teams.listTeams().map(t => ({
1699
+ ...t,
1700
+ memberKeys: t.memberKeys.map(k => k.slice(0, 7) + '...' + k.slice(-4)),
1701
+ }));
1702
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1703
+ res.end(JSON.stringify({ teams, count: teams.length }));
1704
+ }
1705
+ async handleCreateTeam(req, res) {
1706
+ if (!this.checkAdmin(req, res))
1707
+ return;
1708
+ if (req.method !== 'POST') {
1709
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1710
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
1711
+ return;
1712
+ }
1713
+ const body = await this.readBody(req);
1714
+ let params;
1715
+ try {
1716
+ params = JSON.parse(body);
1717
+ }
1718
+ catch {
1719
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1720
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
1721
+ return;
1722
+ }
1723
+ if (!params.name || typeof params.name !== 'string') {
1724
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1725
+ res.end(JSON.stringify({ error: 'Missing required field: name' }));
1726
+ return;
1727
+ }
1728
+ const team = this.teams.createTeam({
1729
+ name: params.name,
1730
+ description: params.description,
1731
+ budget: params.budget,
1732
+ quota: params.quota,
1733
+ tags: params.tags,
1734
+ });
1735
+ this.audit.log('team.created', 'admin', `Team created: ${team.name}`, {
1736
+ teamId: team.id,
1737
+ teamName: team.name,
1738
+ budget: team.budget,
1739
+ });
1740
+ this.gate.store.save();
1741
+ res.writeHead(201, { 'Content-Type': 'application/json' });
1742
+ res.end(JSON.stringify({ message: 'Team created', team }));
1743
+ }
1744
+ async handleUpdateTeam(req, res) {
1745
+ if (!this.checkAdmin(req, res))
1746
+ return;
1747
+ if (req.method !== 'POST') {
1748
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1749
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
1750
+ return;
1751
+ }
1752
+ const body = await this.readBody(req);
1753
+ let params;
1754
+ try {
1755
+ params = JSON.parse(body);
1756
+ }
1757
+ catch {
1758
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1759
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
1760
+ return;
1761
+ }
1762
+ if (!params.teamId || typeof params.teamId !== 'string') {
1763
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1764
+ res.end(JSON.stringify({ error: 'Missing required field: teamId' }));
1765
+ return;
1766
+ }
1767
+ const success = this.teams.updateTeam(params.teamId, {
1768
+ name: params.name,
1769
+ description: params.description,
1770
+ budget: params.budget,
1771
+ quota: params.quota,
1772
+ tags: params.tags,
1773
+ });
1774
+ if (!success) {
1775
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1776
+ res.end(JSON.stringify({ error: 'Team not found or inactive' }));
1777
+ return;
1778
+ }
1779
+ this.audit.log('team.updated', 'admin', `Team updated: ${params.teamId}`, { teamId: params.teamId });
1780
+ this.gate.store.save();
1781
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1782
+ res.end(JSON.stringify({ message: 'Team updated', team: this.teams.getTeam(params.teamId) }));
1783
+ }
1784
+ async handleDeleteTeam(req, res) {
1785
+ if (!this.checkAdmin(req, res))
1786
+ return;
1787
+ if (req.method !== 'POST') {
1788
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1789
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
1790
+ return;
1791
+ }
1792
+ const body = await this.readBody(req);
1793
+ let params;
1794
+ try {
1795
+ params = JSON.parse(body);
1796
+ }
1797
+ catch {
1798
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1799
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
1800
+ return;
1801
+ }
1802
+ if (!params.teamId || typeof params.teamId !== 'string') {
1803
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1804
+ res.end(JSON.stringify({ error: 'Missing required field: teamId' }));
1805
+ return;
1806
+ }
1807
+ const success = this.teams.deleteTeam(params.teamId);
1808
+ if (!success) {
1809
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1810
+ res.end(JSON.stringify({ error: 'Team not found or already deleted' }));
1811
+ return;
1812
+ }
1813
+ this.audit.log('team.deleted', 'admin', `Team deleted: ${params.teamId}`, { teamId: params.teamId });
1814
+ this.gate.store.save();
1815
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1816
+ res.end(JSON.stringify({ message: 'Team deleted' }));
1817
+ }
1818
+ async handleTeamAssignKey(req, res) {
1819
+ if (!this.checkAdmin(req, res))
1820
+ return;
1821
+ if (req.method !== 'POST') {
1822
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1823
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
1824
+ return;
1825
+ }
1826
+ const body = await this.readBody(req);
1827
+ let params;
1828
+ try {
1829
+ params = JSON.parse(body);
1830
+ }
1831
+ catch {
1832
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1833
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
1834
+ return;
1835
+ }
1836
+ if (!params.teamId || typeof params.teamId !== 'string') {
1837
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1838
+ res.end(JSON.stringify({ error: 'Missing required field: teamId' }));
1839
+ return;
1840
+ }
1841
+ if (!params.key || typeof params.key !== 'string') {
1842
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1843
+ res.end(JSON.stringify({ error: 'Missing required field: key' }));
1844
+ return;
1845
+ }
1846
+ // Verify the key exists
1847
+ const keyRecord = this.gate.store.getKey(params.key);
1848
+ if (!keyRecord) {
1849
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1850
+ res.end(JSON.stringify({ error: 'API key not found' }));
1851
+ return;
1852
+ }
1853
+ const result = this.teams.assignKey(params.teamId, params.key);
1854
+ if (!result.success) {
1855
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1856
+ res.end(JSON.stringify({ error: result.error }));
1857
+ return;
1858
+ }
1859
+ this.audit.log('team.key_assigned', 'admin', `Key assigned to team ${params.teamId}`, {
1860
+ teamId: params.teamId,
1861
+ keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1862
+ });
1863
+ this.gate.store.save();
1864
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1865
+ res.end(JSON.stringify({ message: 'Key assigned to team' }));
1866
+ }
1867
+ async handleTeamRemoveKey(req, res) {
1868
+ if (!this.checkAdmin(req, res))
1869
+ return;
1870
+ if (req.method !== 'POST') {
1871
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1872
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
1873
+ return;
1874
+ }
1875
+ const body = await this.readBody(req);
1876
+ let params;
1877
+ try {
1878
+ params = JSON.parse(body);
1879
+ }
1880
+ catch {
1881
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1882
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
1883
+ return;
1884
+ }
1885
+ if (!params.teamId || typeof params.teamId !== 'string') {
1886
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1887
+ res.end(JSON.stringify({ error: 'Missing required field: teamId' }));
1888
+ return;
1889
+ }
1890
+ if (!params.key || typeof params.key !== 'string') {
1891
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1892
+ res.end(JSON.stringify({ error: 'Missing required field: key' }));
1893
+ return;
1894
+ }
1895
+ const success = this.teams.removeKey(params.teamId, params.key);
1896
+ if (!success) {
1897
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1898
+ res.end(JSON.stringify({ error: 'Key not found in team' }));
1899
+ return;
1900
+ }
1901
+ this.audit.log('team.key_removed', 'admin', `Key removed from team ${params.teamId}`, {
1902
+ teamId: params.teamId,
1903
+ keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1904
+ });
1905
+ this.gate.store.save();
1906
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1907
+ res.end(JSON.stringify({ message: 'Key removed from team' }));
1908
+ }
1909
+ handleTeamUsage(req, res) {
1910
+ if (!this.checkAdmin(req, res))
1911
+ return;
1912
+ if (req.method !== 'GET') {
1913
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1914
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
1915
+ return;
1916
+ }
1917
+ const url = new URL(req.url || '/', `http://localhost`);
1918
+ const teamId = url.searchParams.get('teamId');
1919
+ if (!teamId) {
1920
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1921
+ res.end(JSON.stringify({ error: 'Missing required query param: teamId' }));
1922
+ return;
1923
+ }
1924
+ const summary = this.teams.getUsageSummary(teamId, (key) => this.gate.store.getKey(key));
1925
+ if (!summary) {
1926
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1927
+ res.end(JSON.stringify({ error: 'Team not found' }));
1928
+ return;
1929
+ }
1930
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1931
+ res.end(JSON.stringify(summary));
1932
+ }
1632
1933
  readBody(req) {
1633
1934
  return new Promise((resolve, reject) => {
1634
1935
  let body = '';
@@ -1652,6 +1953,9 @@ class PayGateServer {
1652
1953
  this.oauth?.destroy();
1653
1954
  this.sessions.destroy();
1654
1955
  this.audit.destroy();
1956
+ if (this.redisSync) {
1957
+ await this.redisSync.destroy();
1958
+ }
1655
1959
  if (this.server) {
1656
1960
  return new Promise((resolve) => {
1657
1961
  this.server.close(() => resolve());