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/README.md +138 -3
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/gate.d.ts +7 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +19 -0
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/redis-client.d.ts +84 -0
- package/dist/redis-client.d.ts.map +1 -0
- package/dist/redis-client.js +341 -0
- package/dist/redis-client.js.map +1 -0
- package/dist/redis-sync.d.ts +65 -0
- package/dist/redis-sync.d.ts.map +1 -0
- package/dist/redis-sync.js +280 -0
- package/dist/redis-sync.js.map +1 -0
- package/dist/server.d.ts +14 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +305 -1
- package/dist/server.js.map +1 -1
- package/dist/teams.d.ts +158 -0
- package/dist/teams.d.ts.map +1 -0
- package/dist/teams.js +328 -0
- package/dist/teams.js.map +1 -0
- package/package.json +2 -2
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());
|