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/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
@@ -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.adminKey.length >= 8
180
- ? this.adminKey
181
- : this.adminKey + require('crypto').randomBytes(8).toString('hex');
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.adminKey });
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
- if (adminKey !== this.adminKey) {
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).