paygate-mcp 3.1.0 → 3.3.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 +116 -0
- package/dist/admin-keys.d.ts +89 -0
- package/dist/admin-keys.d.ts.map +1 -0
- package/dist/admin-keys.js +178 -0
- package/dist/admin-keys.js.map +1 -0
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/gate.d.ts +8 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +35 -0
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/redis-sync.d.ts.map +1 -1
- package/dist/redis-sync.js +6 -0
- package/dist/redis-sync.js.map +1 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +272 -28
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +9 -0
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -46,6 +46,7 @@ const teams_1 = require("./teams");
|
|
|
46
46
|
const redis_client_1 = require("./redis-client");
|
|
47
47
|
const redis_sync_1 = require("./redis-sync");
|
|
48
48
|
const tokens_1 = require("./tokens");
|
|
49
|
+
const admin_keys_1 = require("./admin-keys");
|
|
49
50
|
/** Max request body size: 1MB */
|
|
50
51
|
const MAX_BODY_SIZE = 1_048_576;
|
|
51
52
|
class PayGateServer {
|
|
@@ -56,7 +57,10 @@ class PayGateServer {
|
|
|
56
57
|
router;
|
|
57
58
|
server = null;
|
|
58
59
|
config;
|
|
59
|
-
|
|
60
|
+
/** Admin key manager (multiple keys with role-based permissions) */
|
|
61
|
+
adminKeys;
|
|
62
|
+
/** The bootstrap admin key (from constructor or auto-generated) */
|
|
63
|
+
bootstrapAdminKey;
|
|
60
64
|
stripeHandler = null;
|
|
61
65
|
/** OAuth 2.1 provider (null if OAuth is not enabled) */
|
|
62
66
|
oauth = null;
|
|
@@ -90,7 +94,11 @@ class PayGateServer {
|
|
|
90
94
|
}
|
|
91
95
|
constructor(config, adminKey, statePath, remoteUrl, stripeWebhookSecret, servers, redisUrl) {
|
|
92
96
|
this.config = { ...types_1.DEFAULT_CONFIG, ...config };
|
|
93
|
-
this.
|
|
97
|
+
this.bootstrapAdminKey = adminKey || `admin_${require('crypto').randomBytes(16).toString('hex')}`;
|
|
98
|
+
// Admin key manager with file persistence (separate from API key state)
|
|
99
|
+
const adminStatePath = statePath ? statePath.replace(/\.json$/, '-admin.json') : undefined;
|
|
100
|
+
this.adminKeys = new admin_keys_1.AdminKeyManager(adminStatePath);
|
|
101
|
+
this.adminKeys.bootstrap(this.bootstrapAdminKey);
|
|
94
102
|
this.gate = new gate_1.Gate(this.config, statePath);
|
|
95
103
|
// Multi-server mode: use Router
|
|
96
104
|
if (servers && servers.length > 0) {
|
|
@@ -136,6 +144,9 @@ class PayGateServer {
|
|
|
136
144
|
this.metrics.registerGauge('paygate_total_credits_available', 'Total credits across all active keys', () => {
|
|
137
145
|
return this.gate.store.listKeys().filter(k => k.active).reduce((sum, k) => sum + k.credits, 0);
|
|
138
146
|
});
|
|
147
|
+
this.metrics.registerGauge('paygate_admin_keys_total', 'Number of active admin keys', () => {
|
|
148
|
+
return this.adminKeys.activeCount;
|
|
149
|
+
});
|
|
139
150
|
// Analytics engine
|
|
140
151
|
this.analytics = new analytics_1.AnalyticsEngine();
|
|
141
152
|
// Alert engine
|
|
@@ -161,10 +172,24 @@ class PayGateServer {
|
|
|
161
172
|
this.gate.teamRecorder = (apiKey, credits) => {
|
|
162
173
|
this.teams.recordUsage(apiKey, credits);
|
|
163
174
|
};
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
// Wire auto-topup hook: audit log + webhook + Redis sync
|
|
176
|
+
this.gate.onAutoTopup = (apiKey, amount, newBalance) => {
|
|
177
|
+
const keyMasked = (0, audit_1.maskKeyForAudit)(apiKey);
|
|
178
|
+
this.audit.log('key.auto_topped_up', 'system', `Auto-topup: added ${amount} credits`, {
|
|
179
|
+
keyMasked, creditsAdded: amount, newBalance,
|
|
180
|
+
});
|
|
181
|
+
this.gate.webhook?.emitAdmin('key.auto_topped_up', 'system', {
|
|
182
|
+
keyMasked, creditsAdded: amount, newBalance,
|
|
183
|
+
});
|
|
184
|
+
// Sync to Redis (if configured)
|
|
185
|
+
if (this.redisSync) {
|
|
186
|
+
this.redisSync.atomicTopup(apiKey, amount).catch(() => { });
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
// Scoped token manager (uses bootstrap admin key as signing secret, padded to min length)
|
|
190
|
+
const tokenSecret = this.bootstrapAdminKey.length >= 8
|
|
191
|
+
? this.bootstrapAdminKey
|
|
192
|
+
: this.bootstrapAdminKey + require('crypto').randomBytes(8).toString('hex');
|
|
168
193
|
this.tokens = new tokens_1.ScopedTokenManager(tokenSecret);
|
|
169
194
|
// Redis distributed state (if configured)
|
|
170
195
|
if (redisUrl) {
|
|
@@ -208,7 +233,7 @@ class PayGateServer {
|
|
|
208
233
|
this.server.listen(this.config.port, () => {
|
|
209
234
|
const addr = this.server.address();
|
|
210
235
|
const actualPort = typeof addr === 'object' && addr ? addr.port : this.config.port;
|
|
211
|
-
resolve({ port: actualPort, adminKey: this.
|
|
236
|
+
resolve({ port: actualPort, adminKey: this.bootstrapAdminKey });
|
|
212
237
|
});
|
|
213
238
|
this.server.on('error', reject);
|
|
214
239
|
});
|
|
@@ -259,6 +284,8 @@ class PayGateServer {
|
|
|
259
284
|
return this.handleSetIpAllowlist(req, res);
|
|
260
285
|
case '/keys/search':
|
|
261
286
|
return this.handleSearchKeysByTag(req, res);
|
|
287
|
+
case '/keys/auto-topup':
|
|
288
|
+
return this.handleSetAutoTopup(req, res);
|
|
262
289
|
case '/topup':
|
|
263
290
|
return this.handleTopUp(req, res);
|
|
264
291
|
case '/balance':
|
|
@@ -330,6 +357,15 @@ class PayGateServer {
|
|
|
330
357
|
return this.handleRevokeToken(req, res);
|
|
331
358
|
case '/tokens/revoked':
|
|
332
359
|
return this.handleListRevokedTokens(req, res);
|
|
360
|
+
// ─── Admin key management endpoints ──────────────────────────────
|
|
361
|
+
case '/admin/keys':
|
|
362
|
+
if (req.method === 'POST')
|
|
363
|
+
return this.handleCreateAdminKey(req, res);
|
|
364
|
+
if (req.method === 'GET')
|
|
365
|
+
return this.handleListAdminKeys(req, res);
|
|
366
|
+
break;
|
|
367
|
+
case '/admin/keys/revoke':
|
|
368
|
+
return this.handleRevokeAdminKey(req, res);
|
|
333
369
|
// ─── OAuth 2.1 endpoints ─────────────────────────────────────────
|
|
334
370
|
case '/.well-known/oauth-authorization-server':
|
|
335
371
|
return this.handleOAuthMetadata(req, res);
|
|
@@ -701,6 +737,7 @@ class PayGateServer {
|
|
|
701
737
|
setTags: 'POST /keys/tags — Set key tags/metadata (requires X-Admin-Key)',
|
|
702
738
|
setIpAllowlist: 'POST /keys/ip — Set IP allowlist (requires X-Admin-Key)',
|
|
703
739
|
searchKeys: 'POST /keys/search — Search keys by tags (requires X-Admin-Key)',
|
|
740
|
+
autoTopup: 'POST /keys/auto-topup — Configure auto-topup for a key (requires X-Admin-Key)',
|
|
704
741
|
pricing: 'GET /pricing — Tool pricing breakdown (public)',
|
|
705
742
|
mcpPayment: 'GET /.well-known/mcp-payment — Payment metadata (SEP-2007)',
|
|
706
743
|
audit: 'GET /audit — Query audit log (requires X-Admin-Key)',
|
|
@@ -720,6 +757,9 @@ class PayGateServer {
|
|
|
720
757
|
createToken: 'POST /tokens — Create scoped token (requires X-Admin-Key)',
|
|
721
758
|
revokeToken: 'POST /tokens/revoke — Revoke a scoped token (requires X-Admin-Key)',
|
|
722
759
|
listRevokedTokens: 'GET /tokens/revoked — List revoked tokens (requires X-Admin-Key)',
|
|
760
|
+
adminKeys: 'GET /admin/keys — List admin keys (requires X-Admin-Key, super_admin)',
|
|
761
|
+
createAdminKey: 'POST /admin/keys — Create admin key with role (requires X-Admin-Key, super_admin)',
|
|
762
|
+
revokeAdminKey: 'POST /admin/keys/revoke — Revoke an admin key (requires X-Admin-Key, super_admin)',
|
|
723
763
|
...(this.oauth ? {
|
|
724
764
|
oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
|
|
725
765
|
oauthRegister: 'POST /oauth/register — Register OAuth client',
|
|
@@ -781,7 +821,7 @@ class PayGateServer {
|
|
|
781
821
|
}
|
|
782
822
|
// ─── /keys — Create ─────────────────────────────────────────────────────────
|
|
783
823
|
async handleCreateKey(req, res) {
|
|
784
|
-
if (!this.checkAdmin(req, res))
|
|
824
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
785
825
|
return;
|
|
786
826
|
const body = await this.readBody(req);
|
|
787
827
|
let params;
|
|
@@ -874,7 +914,7 @@ class PayGateServer {
|
|
|
874
914
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
875
915
|
return;
|
|
876
916
|
}
|
|
877
|
-
if (!this.checkAdmin(req, res))
|
|
917
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
878
918
|
return;
|
|
879
919
|
const body = await this.readBody(req);
|
|
880
920
|
let params;
|
|
@@ -929,7 +969,7 @@ class PayGateServer {
|
|
|
929
969
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
930
970
|
return;
|
|
931
971
|
}
|
|
932
|
-
if (!this.checkAdmin(req, res))
|
|
972
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
933
973
|
return;
|
|
934
974
|
const body = await this.readBody(req);
|
|
935
975
|
let params;
|
|
@@ -975,7 +1015,7 @@ class PayGateServer {
|
|
|
975
1015
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
976
1016
|
return;
|
|
977
1017
|
}
|
|
978
|
-
if (!this.checkAdmin(req, res))
|
|
1018
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
979
1019
|
return;
|
|
980
1020
|
const body = await this.readBody(req);
|
|
981
1021
|
let params;
|
|
@@ -1027,7 +1067,7 @@ class PayGateServer {
|
|
|
1027
1067
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1028
1068
|
return;
|
|
1029
1069
|
}
|
|
1030
|
-
if (!this.checkAdmin(req, res))
|
|
1070
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1031
1071
|
return;
|
|
1032
1072
|
const body = await this.readBody(req);
|
|
1033
1073
|
let params;
|
|
@@ -1071,7 +1111,7 @@ class PayGateServer {
|
|
|
1071
1111
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1072
1112
|
return;
|
|
1073
1113
|
}
|
|
1074
|
-
if (!this.checkAdmin(req, res))
|
|
1114
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1075
1115
|
return;
|
|
1076
1116
|
const body = await this.readBody(req);
|
|
1077
1117
|
let params;
|
|
@@ -1121,7 +1161,7 @@ class PayGateServer {
|
|
|
1121
1161
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1122
1162
|
return;
|
|
1123
1163
|
}
|
|
1124
|
-
if (!this.checkAdmin(req, res))
|
|
1164
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1125
1165
|
return;
|
|
1126
1166
|
const body = await this.readBody(req);
|
|
1127
1167
|
let params;
|
|
@@ -1177,7 +1217,7 @@ class PayGateServer {
|
|
|
1177
1217
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1178
1218
|
return;
|
|
1179
1219
|
}
|
|
1180
|
-
if (!this.checkAdmin(req, res))
|
|
1220
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1181
1221
|
return;
|
|
1182
1222
|
const body = await this.readBody(req);
|
|
1183
1223
|
let params;
|
|
@@ -1224,7 +1264,7 @@ class PayGateServer {
|
|
|
1224
1264
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1225
1265
|
return;
|
|
1226
1266
|
}
|
|
1227
|
-
if (!this.checkAdmin(req, res))
|
|
1267
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1228
1268
|
return;
|
|
1229
1269
|
const body = await this.readBody(req);
|
|
1230
1270
|
let params;
|
|
@@ -1292,6 +1332,79 @@ class PayGateServer {
|
|
|
1292
1332
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1293
1333
|
res.end(JSON.stringify({ keys: results, count: results.length }));
|
|
1294
1334
|
}
|
|
1335
|
+
// ─── /keys/auto-topup — Configure auto-topup ────────────────────────────────
|
|
1336
|
+
async handleSetAutoTopup(req, res) {
|
|
1337
|
+
if (req.method !== 'POST') {
|
|
1338
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
1339
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1343
|
+
return;
|
|
1344
|
+
const body = await this.readBody(req);
|
|
1345
|
+
let params;
|
|
1346
|
+
try {
|
|
1347
|
+
params = JSON.parse(body);
|
|
1348
|
+
}
|
|
1349
|
+
catch {
|
|
1350
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1351
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (!params.key) {
|
|
1355
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1356
|
+
res.end(JSON.stringify({ error: 'Missing key' }));
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const record = this.gate.store.getKey(params.key);
|
|
1360
|
+
if (!record) {
|
|
1361
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1362
|
+
res.end(JSON.stringify({ error: 'Key not found or inactive' }));
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
// Disable auto-topup
|
|
1366
|
+
if (params.disable) {
|
|
1367
|
+
record.autoTopup = undefined;
|
|
1368
|
+
this.gate.store.save();
|
|
1369
|
+
this.syncKeyMutation(params.key);
|
|
1370
|
+
this.audit.log('key.auto_topup_configured', 'admin', 'Auto-topup disabled', {
|
|
1371
|
+
keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
|
|
1372
|
+
});
|
|
1373
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1374
|
+
res.end(JSON.stringify({ autoTopup: null, message: 'Auto-topup disabled' }));
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
// Validate params
|
|
1378
|
+
const threshold = Math.max(0, Math.floor(Number(params.threshold) || 0));
|
|
1379
|
+
const amount = Math.max(0, Math.floor(Number(params.amount) || 0));
|
|
1380
|
+
const maxDaily = Math.max(0, Math.floor(Number(params.maxDaily) || 0));
|
|
1381
|
+
if (threshold <= 0) {
|
|
1382
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1383
|
+
res.end(JSON.stringify({ error: 'threshold must be a positive integer' }));
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
if (amount <= 0) {
|
|
1387
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1388
|
+
res.end(JSON.stringify({ error: 'amount must be a positive integer' }));
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
record.autoTopup = { threshold, amount, maxDaily };
|
|
1392
|
+
this.gate.store.save();
|
|
1393
|
+
this.syncKeyMutation(params.key);
|
|
1394
|
+
this.audit.log('key.auto_topup_configured', 'admin', `Auto-topup configured: threshold=${threshold}, amount=${amount}, maxDaily=${maxDaily}`, {
|
|
1395
|
+
keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
|
|
1396
|
+
threshold, amount, maxDaily,
|
|
1397
|
+
});
|
|
1398
|
+
this.gate.webhook?.emitAdmin('key.auto_topup_configured', 'admin', {
|
|
1399
|
+
keyMasked: (0, audit_1.maskKeyForAudit)(params.key), threshold, amount, maxDaily,
|
|
1400
|
+
});
|
|
1401
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1402
|
+
res.end(JSON.stringify({
|
|
1403
|
+
autoTopup: record.autoTopup,
|
|
1404
|
+
message: `Auto-topup enabled: add ${amount} credits when balance drops below ${threshold}` +
|
|
1405
|
+
(maxDaily > 0 ? ` (max ${maxDaily}/day)` : ' (unlimited daily)'),
|
|
1406
|
+
}));
|
|
1407
|
+
}
|
|
1295
1408
|
// ─── /balance — Client self-service ────────────────────────────────────────
|
|
1296
1409
|
handleBalance(req, res) {
|
|
1297
1410
|
if (req.method !== 'GET') {
|
|
@@ -1347,7 +1460,7 @@ class PayGateServer {
|
|
|
1347
1460
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1348
1461
|
return;
|
|
1349
1462
|
}
|
|
1350
|
-
if (!this.checkAdmin(req, res))
|
|
1463
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1351
1464
|
return;
|
|
1352
1465
|
const body = await this.readBody(req);
|
|
1353
1466
|
let params;
|
|
@@ -1792,7 +1905,7 @@ class PayGateServer {
|
|
|
1792
1905
|
}));
|
|
1793
1906
|
}
|
|
1794
1907
|
async handleConfigureAlerts(req, res) {
|
|
1795
|
-
if (!this.checkAdmin(req, res))
|
|
1908
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1796
1909
|
return;
|
|
1797
1910
|
const body = await this.readBody(req);
|
|
1798
1911
|
let params;
|
|
@@ -1852,7 +1965,7 @@ class PayGateServer {
|
|
|
1852
1965
|
}));
|
|
1853
1966
|
}
|
|
1854
1967
|
handleClearDeadLetters(req, res) {
|
|
1855
|
-
if (!this.checkAdmin(req, res))
|
|
1968
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1856
1969
|
return;
|
|
1857
1970
|
if (!this.gate.webhook) {
|
|
1858
1971
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -1958,9 +2071,10 @@ class PayGateServer {
|
|
|
1958
2071
|
res.end(JSON.stringify(this.audit.stats(), null, 2));
|
|
1959
2072
|
}
|
|
1960
2073
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
1961
|
-
checkAdmin(req, res) {
|
|
2074
|
+
checkAdmin(req, res, minRole) {
|
|
1962
2075
|
const adminKey = req.headers['x-admin-key'];
|
|
1963
|
-
|
|
2076
|
+
const record = adminKey ? this.adminKeys.validate(adminKey) : null;
|
|
2077
|
+
if (!record) {
|
|
1964
2078
|
this.audit.log('admin.auth_failed', 'unknown', `Admin auth failed on ${req.url}`, {
|
|
1965
2079
|
url: req.url,
|
|
1966
2080
|
method: req.method,
|
|
@@ -1969,6 +2083,18 @@ class PayGateServer {
|
|
|
1969
2083
|
res.end(JSON.stringify({ error: 'Invalid admin key' }));
|
|
1970
2084
|
return false;
|
|
1971
2085
|
}
|
|
2086
|
+
// Role-based permission check (if a minimum role is specified)
|
|
2087
|
+
if (minRole && admin_keys_1.ROLE_HIERARCHY[record.role] < admin_keys_1.ROLE_HIERARCHY[minRole]) {
|
|
2088
|
+
this.audit.log('admin.auth_failed', adminKey.slice(0, 7) + '...' + adminKey.slice(-4), `Insufficient role for ${req.url} (need ${minRole}, have ${record.role})`, {
|
|
2089
|
+
url: req.url,
|
|
2090
|
+
method: req.method,
|
|
2091
|
+
requiredRole: minRole,
|
|
2092
|
+
currentRole: record.role,
|
|
2093
|
+
});
|
|
2094
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2095
|
+
res.end(JSON.stringify({ error: 'Insufficient permissions', requiredRole: minRole, currentRole: record.role }));
|
|
2096
|
+
return false;
|
|
2097
|
+
}
|
|
1972
2098
|
return true;
|
|
1973
2099
|
}
|
|
1974
2100
|
// ─── /teams — Team management ────────────────────────────────────────────
|
|
@@ -1988,7 +2114,7 @@ class PayGateServer {
|
|
|
1988
2114
|
res.end(JSON.stringify({ teams, count: teams.length }));
|
|
1989
2115
|
}
|
|
1990
2116
|
async handleCreateTeam(req, res) {
|
|
1991
|
-
if (!this.checkAdmin(req, res))
|
|
2117
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1992
2118
|
return;
|
|
1993
2119
|
if (req.method !== 'POST') {
|
|
1994
2120
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2027,7 +2153,7 @@ class PayGateServer {
|
|
|
2027
2153
|
res.end(JSON.stringify({ message: 'Team created', team }));
|
|
2028
2154
|
}
|
|
2029
2155
|
async handleUpdateTeam(req, res) {
|
|
2030
|
-
if (!this.checkAdmin(req, res))
|
|
2156
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2031
2157
|
return;
|
|
2032
2158
|
if (req.method !== 'POST') {
|
|
2033
2159
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2067,7 +2193,7 @@ class PayGateServer {
|
|
|
2067
2193
|
res.end(JSON.stringify({ message: 'Team updated', team: this.teams.getTeam(params.teamId) }));
|
|
2068
2194
|
}
|
|
2069
2195
|
async handleDeleteTeam(req, res) {
|
|
2070
|
-
if (!this.checkAdmin(req, res))
|
|
2196
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2071
2197
|
return;
|
|
2072
2198
|
if (req.method !== 'POST') {
|
|
2073
2199
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2101,7 +2227,7 @@ class PayGateServer {
|
|
|
2101
2227
|
res.end(JSON.stringify({ message: 'Team deleted' }));
|
|
2102
2228
|
}
|
|
2103
2229
|
async handleTeamAssignKey(req, res) {
|
|
2104
|
-
if (!this.checkAdmin(req, res))
|
|
2230
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2105
2231
|
return;
|
|
2106
2232
|
if (req.method !== 'POST') {
|
|
2107
2233
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2150,7 +2276,7 @@ class PayGateServer {
|
|
|
2150
2276
|
res.end(JSON.stringify({ message: 'Key assigned to team' }));
|
|
2151
2277
|
}
|
|
2152
2278
|
async handleTeamRemoveKey(req, res) {
|
|
2153
|
-
if (!this.checkAdmin(req, res))
|
|
2279
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2154
2280
|
return;
|
|
2155
2281
|
if (req.method !== 'POST') {
|
|
2156
2282
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2230,7 +2356,7 @@ class PayGateServer {
|
|
|
2230
2356
|
}
|
|
2231
2357
|
// ─── /tokens — Create scoped token ──────────────────────────────────────────
|
|
2232
2358
|
async handleCreateToken(req, res) {
|
|
2233
|
-
if (!this.checkAdmin(req, res))
|
|
2359
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2234
2360
|
return;
|
|
2235
2361
|
const body = await this.readBody(req);
|
|
2236
2362
|
let params;
|
|
@@ -2285,7 +2411,7 @@ class PayGateServer {
|
|
|
2285
2411
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
2286
2412
|
return;
|
|
2287
2413
|
}
|
|
2288
|
-
if (!this.checkAdmin(req, res))
|
|
2414
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2289
2415
|
return;
|
|
2290
2416
|
const body = await this.readBody(req);
|
|
2291
2417
|
let params;
|
|
@@ -2357,6 +2483,124 @@ class PayGateServer {
|
|
|
2357
2483
|
})),
|
|
2358
2484
|
}));
|
|
2359
2485
|
}
|
|
2486
|
+
// ─── /admin/keys — Admin key management ────────────────────────────────────
|
|
2487
|
+
handleListAdminKeys(req, res) {
|
|
2488
|
+
if (req.method !== 'GET') {
|
|
2489
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
2490
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
if (!this.checkAdmin(req, res, 'super_admin'))
|
|
2494
|
+
return;
|
|
2495
|
+
const keys = this.adminKeys.list().map(k => ({
|
|
2496
|
+
key: k.key.slice(0, 7) + '...' + k.key.slice(-4),
|
|
2497
|
+
name: k.name,
|
|
2498
|
+
role: k.role,
|
|
2499
|
+
createdAt: k.createdAt,
|
|
2500
|
+
createdBy: k.createdBy,
|
|
2501
|
+
active: k.active,
|
|
2502
|
+
lastUsedAt: k.lastUsedAt,
|
|
2503
|
+
}));
|
|
2504
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2505
|
+
res.end(JSON.stringify({ count: keys.length, keys }));
|
|
2506
|
+
}
|
|
2507
|
+
async handleCreateAdminKey(req, res) {
|
|
2508
|
+
if (req.method !== 'POST') {
|
|
2509
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
2510
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
if (!this.checkAdmin(req, res, 'super_admin'))
|
|
2514
|
+
return;
|
|
2515
|
+
const body = await this.readBody(req);
|
|
2516
|
+
let params;
|
|
2517
|
+
try {
|
|
2518
|
+
params = JSON.parse(body);
|
|
2519
|
+
}
|
|
2520
|
+
catch {
|
|
2521
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2522
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
if (!params.name || typeof params.name !== 'string') {
|
|
2526
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2527
|
+
res.end(JSON.stringify({ error: 'Missing required field: name' }));
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
const role = (params.role || 'admin');
|
|
2531
|
+
if (!['super_admin', 'admin', 'viewer'].includes(role)) {
|
|
2532
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2533
|
+
res.end(JSON.stringify({ error: 'Invalid role. Must be super_admin, admin, or viewer.' }));
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
// Mask the requesting admin key for audit
|
|
2537
|
+
const callerKey = req.headers['x-admin-key'];
|
|
2538
|
+
const callerMasked = callerKey.slice(0, 7) + '...' + callerKey.slice(-4);
|
|
2539
|
+
const record = this.adminKeys.create(params.name, role, callerMasked);
|
|
2540
|
+
this.audit.log('admin_key.created', callerMasked, `Created admin key "${params.name}" with role ${role}`, {
|
|
2541
|
+
newKeyMasked: record.key.slice(0, 7) + '...' + record.key.slice(-4),
|
|
2542
|
+
role,
|
|
2543
|
+
});
|
|
2544
|
+
this.gate.webhook?.emitAdmin('admin_key.created', callerMasked, {
|
|
2545
|
+
newKeyMasked: record.key.slice(0, 7) + '...' + record.key.slice(-4),
|
|
2546
|
+
name: params.name,
|
|
2547
|
+
role,
|
|
2548
|
+
});
|
|
2549
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
2550
|
+
res.end(JSON.stringify({
|
|
2551
|
+
key: record.key,
|
|
2552
|
+
name: record.name,
|
|
2553
|
+
role: record.role,
|
|
2554
|
+
createdAt: record.createdAt,
|
|
2555
|
+
}));
|
|
2556
|
+
}
|
|
2557
|
+
async handleRevokeAdminKey(req, res) {
|
|
2558
|
+
if (req.method !== 'POST') {
|
|
2559
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
2560
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
if (!this.checkAdmin(req, res, 'super_admin'))
|
|
2564
|
+
return;
|
|
2565
|
+
const body = await this.readBody(req);
|
|
2566
|
+
let params;
|
|
2567
|
+
try {
|
|
2568
|
+
params = JSON.parse(body);
|
|
2569
|
+
}
|
|
2570
|
+
catch {
|
|
2571
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2572
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (!params.key || typeof params.key !== 'string') {
|
|
2576
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2577
|
+
res.end(JSON.stringify({ error: 'Missing required field: key' }));
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
const callerKey = req.headers['x-admin-key'];
|
|
2581
|
+
// Prevent revoking your own key
|
|
2582
|
+
if (params.key === callerKey) {
|
|
2583
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2584
|
+
res.end(JSON.stringify({ error: 'Cannot revoke your own admin key' }));
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const result = this.adminKeys.revoke(params.key);
|
|
2588
|
+
if (!result.success) {
|
|
2589
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2590
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const callerMasked = callerKey.slice(0, 7) + '...' + callerKey.slice(-4);
|
|
2594
|
+
const targetMasked = params.key.slice(0, 7) + '...' + params.key.slice(-4);
|
|
2595
|
+
this.audit.log('admin_key.revoked', callerMasked, `Revoked admin key ${targetMasked}`, {
|
|
2596
|
+
revokedKeyMasked: targetMasked,
|
|
2597
|
+
});
|
|
2598
|
+
this.gate.webhook?.emitAdmin('admin_key.revoked', callerMasked, {
|
|
2599
|
+
revokedKeyMasked: targetMasked,
|
|
2600
|
+
});
|
|
2601
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2602
|
+
res.end(JSON.stringify({ revoked: true }));
|
|
2603
|
+
}
|
|
2360
2604
|
/**
|
|
2361
2605
|
* Sync a key mutation to Redis. Call after any local KeyStore mutation
|
|
2362
2606
|
* (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
|