paygate-mcp 3.2.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 +53 -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/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/server.d.ts +8 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +183 -29
- package/dist/server.js.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
|
|
@@ -175,10 +186,10 @@ class PayGateServer {
|
|
|
175
186
|
this.redisSync.atomicTopup(apiKey, amount).catch(() => { });
|
|
176
187
|
}
|
|
177
188
|
};
|
|
178
|
-
// Scoped token manager (uses admin key as signing secret, padded to min length)
|
|
179
|
-
const tokenSecret = this.
|
|
180
|
-
? this.
|
|
181
|
-
: this.
|
|
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');
|
|
182
193
|
this.tokens = new tokens_1.ScopedTokenManager(tokenSecret);
|
|
183
194
|
// Redis distributed state (if configured)
|
|
184
195
|
if (redisUrl) {
|
|
@@ -222,7 +233,7 @@ class PayGateServer {
|
|
|
222
233
|
this.server.listen(this.config.port, () => {
|
|
223
234
|
const addr = this.server.address();
|
|
224
235
|
const actualPort = typeof addr === 'object' && addr ? addr.port : this.config.port;
|
|
225
|
-
resolve({ port: actualPort, adminKey: this.
|
|
236
|
+
resolve({ port: actualPort, adminKey: this.bootstrapAdminKey });
|
|
226
237
|
});
|
|
227
238
|
this.server.on('error', reject);
|
|
228
239
|
});
|
|
@@ -346,6 +357,15 @@ class PayGateServer {
|
|
|
346
357
|
return this.handleRevokeToken(req, res);
|
|
347
358
|
case '/tokens/revoked':
|
|
348
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);
|
|
349
369
|
// ─── OAuth 2.1 endpoints ─────────────────────────────────────────
|
|
350
370
|
case '/.well-known/oauth-authorization-server':
|
|
351
371
|
return this.handleOAuthMetadata(req, res);
|
|
@@ -737,6 +757,9 @@ class PayGateServer {
|
|
|
737
757
|
createToken: 'POST /tokens — Create scoped token (requires X-Admin-Key)',
|
|
738
758
|
revokeToken: 'POST /tokens/revoke — Revoke a scoped token (requires X-Admin-Key)',
|
|
739
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)',
|
|
740
763
|
...(this.oauth ? {
|
|
741
764
|
oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
|
|
742
765
|
oauthRegister: 'POST /oauth/register — Register OAuth client',
|
|
@@ -798,7 +821,7 @@ class PayGateServer {
|
|
|
798
821
|
}
|
|
799
822
|
// ─── /keys — Create ─────────────────────────────────────────────────────────
|
|
800
823
|
async handleCreateKey(req, res) {
|
|
801
|
-
if (!this.checkAdmin(req, res))
|
|
824
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
802
825
|
return;
|
|
803
826
|
const body = await this.readBody(req);
|
|
804
827
|
let params;
|
|
@@ -891,7 +914,7 @@ class PayGateServer {
|
|
|
891
914
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
892
915
|
return;
|
|
893
916
|
}
|
|
894
|
-
if (!this.checkAdmin(req, res))
|
|
917
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
895
918
|
return;
|
|
896
919
|
const body = await this.readBody(req);
|
|
897
920
|
let params;
|
|
@@ -946,7 +969,7 @@ class PayGateServer {
|
|
|
946
969
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
947
970
|
return;
|
|
948
971
|
}
|
|
949
|
-
if (!this.checkAdmin(req, res))
|
|
972
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
950
973
|
return;
|
|
951
974
|
const body = await this.readBody(req);
|
|
952
975
|
let params;
|
|
@@ -992,7 +1015,7 @@ class PayGateServer {
|
|
|
992
1015
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
993
1016
|
return;
|
|
994
1017
|
}
|
|
995
|
-
if (!this.checkAdmin(req, res))
|
|
1018
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
996
1019
|
return;
|
|
997
1020
|
const body = await this.readBody(req);
|
|
998
1021
|
let params;
|
|
@@ -1044,7 +1067,7 @@ class PayGateServer {
|
|
|
1044
1067
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1045
1068
|
return;
|
|
1046
1069
|
}
|
|
1047
|
-
if (!this.checkAdmin(req, res))
|
|
1070
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1048
1071
|
return;
|
|
1049
1072
|
const body = await this.readBody(req);
|
|
1050
1073
|
let params;
|
|
@@ -1088,7 +1111,7 @@ class PayGateServer {
|
|
|
1088
1111
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1089
1112
|
return;
|
|
1090
1113
|
}
|
|
1091
|
-
if (!this.checkAdmin(req, res))
|
|
1114
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1092
1115
|
return;
|
|
1093
1116
|
const body = await this.readBody(req);
|
|
1094
1117
|
let params;
|
|
@@ -1138,7 +1161,7 @@ class PayGateServer {
|
|
|
1138
1161
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1139
1162
|
return;
|
|
1140
1163
|
}
|
|
1141
|
-
if (!this.checkAdmin(req, res))
|
|
1164
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1142
1165
|
return;
|
|
1143
1166
|
const body = await this.readBody(req);
|
|
1144
1167
|
let params;
|
|
@@ -1194,7 +1217,7 @@ class PayGateServer {
|
|
|
1194
1217
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1195
1218
|
return;
|
|
1196
1219
|
}
|
|
1197
|
-
if (!this.checkAdmin(req, res))
|
|
1220
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1198
1221
|
return;
|
|
1199
1222
|
const body = await this.readBody(req);
|
|
1200
1223
|
let params;
|
|
@@ -1241,7 +1264,7 @@ class PayGateServer {
|
|
|
1241
1264
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1242
1265
|
return;
|
|
1243
1266
|
}
|
|
1244
|
-
if (!this.checkAdmin(req, res))
|
|
1267
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1245
1268
|
return;
|
|
1246
1269
|
const body = await this.readBody(req);
|
|
1247
1270
|
let params;
|
|
@@ -1316,7 +1339,7 @@ class PayGateServer {
|
|
|
1316
1339
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1317
1340
|
return;
|
|
1318
1341
|
}
|
|
1319
|
-
if (!this.checkAdmin(req, res))
|
|
1342
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1320
1343
|
return;
|
|
1321
1344
|
const body = await this.readBody(req);
|
|
1322
1345
|
let params;
|
|
@@ -1437,7 +1460,7 @@ class PayGateServer {
|
|
|
1437
1460
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
1438
1461
|
return;
|
|
1439
1462
|
}
|
|
1440
|
-
if (!this.checkAdmin(req, res))
|
|
1463
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1441
1464
|
return;
|
|
1442
1465
|
const body = await this.readBody(req);
|
|
1443
1466
|
let params;
|
|
@@ -1882,7 +1905,7 @@ class PayGateServer {
|
|
|
1882
1905
|
}));
|
|
1883
1906
|
}
|
|
1884
1907
|
async handleConfigureAlerts(req, res) {
|
|
1885
|
-
if (!this.checkAdmin(req, res))
|
|
1908
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1886
1909
|
return;
|
|
1887
1910
|
const body = await this.readBody(req);
|
|
1888
1911
|
let params;
|
|
@@ -1942,7 +1965,7 @@ class PayGateServer {
|
|
|
1942
1965
|
}));
|
|
1943
1966
|
}
|
|
1944
1967
|
handleClearDeadLetters(req, res) {
|
|
1945
|
-
if (!this.checkAdmin(req, res))
|
|
1968
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
1946
1969
|
return;
|
|
1947
1970
|
if (!this.gate.webhook) {
|
|
1948
1971
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -2048,9 +2071,10 @@ class PayGateServer {
|
|
|
2048
2071
|
res.end(JSON.stringify(this.audit.stats(), null, 2));
|
|
2049
2072
|
}
|
|
2050
2073
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
2051
|
-
checkAdmin(req, res) {
|
|
2074
|
+
checkAdmin(req, res, minRole) {
|
|
2052
2075
|
const adminKey = req.headers['x-admin-key'];
|
|
2053
|
-
|
|
2076
|
+
const record = adminKey ? this.adminKeys.validate(adminKey) : null;
|
|
2077
|
+
if (!record) {
|
|
2054
2078
|
this.audit.log('admin.auth_failed', 'unknown', `Admin auth failed on ${req.url}`, {
|
|
2055
2079
|
url: req.url,
|
|
2056
2080
|
method: req.method,
|
|
@@ -2059,6 +2083,18 @@ class PayGateServer {
|
|
|
2059
2083
|
res.end(JSON.stringify({ error: 'Invalid admin key' }));
|
|
2060
2084
|
return false;
|
|
2061
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
|
+
}
|
|
2062
2098
|
return true;
|
|
2063
2099
|
}
|
|
2064
2100
|
// ─── /teams — Team management ────────────────────────────────────────────
|
|
@@ -2078,7 +2114,7 @@ class PayGateServer {
|
|
|
2078
2114
|
res.end(JSON.stringify({ teams, count: teams.length }));
|
|
2079
2115
|
}
|
|
2080
2116
|
async handleCreateTeam(req, res) {
|
|
2081
|
-
if (!this.checkAdmin(req, res))
|
|
2117
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2082
2118
|
return;
|
|
2083
2119
|
if (req.method !== 'POST') {
|
|
2084
2120
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2117,7 +2153,7 @@ class PayGateServer {
|
|
|
2117
2153
|
res.end(JSON.stringify({ message: 'Team created', team }));
|
|
2118
2154
|
}
|
|
2119
2155
|
async handleUpdateTeam(req, res) {
|
|
2120
|
-
if (!this.checkAdmin(req, res))
|
|
2156
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2121
2157
|
return;
|
|
2122
2158
|
if (req.method !== 'POST') {
|
|
2123
2159
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2157,7 +2193,7 @@ class PayGateServer {
|
|
|
2157
2193
|
res.end(JSON.stringify({ message: 'Team updated', team: this.teams.getTeam(params.teamId) }));
|
|
2158
2194
|
}
|
|
2159
2195
|
async handleDeleteTeam(req, res) {
|
|
2160
|
-
if (!this.checkAdmin(req, res))
|
|
2196
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2161
2197
|
return;
|
|
2162
2198
|
if (req.method !== 'POST') {
|
|
2163
2199
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2191,7 +2227,7 @@ class PayGateServer {
|
|
|
2191
2227
|
res.end(JSON.stringify({ message: 'Team deleted' }));
|
|
2192
2228
|
}
|
|
2193
2229
|
async handleTeamAssignKey(req, res) {
|
|
2194
|
-
if (!this.checkAdmin(req, res))
|
|
2230
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2195
2231
|
return;
|
|
2196
2232
|
if (req.method !== 'POST') {
|
|
2197
2233
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2240,7 +2276,7 @@ class PayGateServer {
|
|
|
2240
2276
|
res.end(JSON.stringify({ message: 'Key assigned to team' }));
|
|
2241
2277
|
}
|
|
2242
2278
|
async handleTeamRemoveKey(req, res) {
|
|
2243
|
-
if (!this.checkAdmin(req, res))
|
|
2279
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2244
2280
|
return;
|
|
2245
2281
|
if (req.method !== 'POST') {
|
|
2246
2282
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2320,7 +2356,7 @@ class PayGateServer {
|
|
|
2320
2356
|
}
|
|
2321
2357
|
// ─── /tokens — Create scoped token ──────────────────────────────────────────
|
|
2322
2358
|
async handleCreateToken(req, res) {
|
|
2323
|
-
if (!this.checkAdmin(req, res))
|
|
2359
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2324
2360
|
return;
|
|
2325
2361
|
const body = await this.readBody(req);
|
|
2326
2362
|
let params;
|
|
@@ -2375,7 +2411,7 @@ class PayGateServer {
|
|
|
2375
2411
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
2376
2412
|
return;
|
|
2377
2413
|
}
|
|
2378
|
-
if (!this.checkAdmin(req, res))
|
|
2414
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2379
2415
|
return;
|
|
2380
2416
|
const body = await this.readBody(req);
|
|
2381
2417
|
let params;
|
|
@@ -2447,6 +2483,124 @@ class PayGateServer {
|
|
|
2447
2483
|
})),
|
|
2448
2484
|
}));
|
|
2449
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
|
+
}
|
|
2450
2604
|
/**
|
|
2451
2605
|
* Sync a key mutation to Redis. Call after any local KeyStore mutation
|
|
2452
2606
|
* (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
|