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/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
- adminKey;
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.adminKey = adminKey || `admin_${require('crypto').randomBytes(16).toString('hex')}`;
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
- // Scoped token manager (uses admin key as signing secret, padded to min length)
165
- const tokenSecret = this.adminKey.length >= 8
166
- ? this.adminKey
167
- : this.adminKey + require('crypto').randomBytes(8).toString('hex');
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.adminKey });
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
- if (adminKey !== this.adminKey) {
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).