paygate-mcp 3.2.0 → 3.4.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.d.ts CHANGED
@@ -27,6 +27,8 @@ import { AlertEngine } from './alerts';
27
27
  import { TeamManager } from './teams';
28
28
  import { RedisSync } from './redis-sync';
29
29
  import { ScopedTokenManager } from './tokens';
30
+ import { AdminKeyManager } from './admin-keys';
31
+ import { PluginManager, PayGatePlugin } from './plugin';
30
32
  /** Union type for both proxy backends */
31
33
  type ProxyBackend = McpProxy | HttpMcpProxy;
32
34
  export declare class PayGateServer {
@@ -37,7 +39,10 @@ export declare class PayGateServer {
37
39
  readonly router: MultiServerRouter | null;
38
40
  private server;
39
41
  private readonly config;
40
- private readonly adminKey;
42
+ /** Admin key manager (multiple keys with role-based permissions) */
43
+ readonly adminKeys: AdminKeyManager;
44
+ /** The bootstrap admin key (from constructor or auto-generated) */
45
+ private readonly bootstrapAdminKey;
41
46
  private stripeHandler;
42
47
  /** OAuth 2.1 provider (null if OAuth is not enabled) */
43
48
  readonly oauth: OAuthProvider | null;
@@ -59,6 +64,8 @@ export declare class PayGateServer {
59
64
  readonly redisSync: RedisSync | null;
60
65
  /** Scoped token manager for short-lived delegated tokens */
61
66
  readonly tokens: ScopedTokenManager;
67
+ /** Plugin manager for extensible middleware hooks */
68
+ readonly plugins: PluginManager;
62
69
  /** Server start time (ms since epoch) */
63
70
  private readonly startedAt;
64
71
  /** Whether the server is draining (shutting down gracefully) */
@@ -70,6 +77,19 @@ export declare class PayGateServer {
70
77
  constructor(config: Partial<PayGateConfig> & {
71
78
  serverCommand: string;
72
79
  }, adminKey?: string, statePath?: string, remoteUrl?: string, stripeWebhookSecret?: string, servers?: ServerBackendConfig[], redisUrl?: string);
80
+ /**
81
+ * Register a plugin for extensible middleware hooks.
82
+ * Plugins run in registration order.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * server.use({
87
+ * name: 'custom-pricing',
88
+ * transformPrice: (tool, base) => tool.startsWith('premium_') ? base * 5 : null,
89
+ * });
90
+ * ```
91
+ */
92
+ use(plugin: PayGatePlugin): this;
73
93
  start(): Promise<{
74
94
  port: number;
75
95
  adminKey: string;
@@ -152,6 +172,10 @@ export declare class PayGateServer {
152
172
  private handleCreateToken;
153
173
  private handleRevokeToken;
154
174
  private handleListRevokedTokens;
175
+ private handleListPlugins;
176
+ private handleListAdminKeys;
177
+ private handleCreateAdminKey;
178
+ private handleRevokeAdminKey;
155
179
  /**
156
180
  * Sync a key mutation to Redis. Call after any local KeyStore mutation
157
181
  * (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -46,6 +46,8 @@ 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");
50
+ const plugin_1 = require("./plugin");
49
51
  /** Max request body size: 1MB */
50
52
  const MAX_BODY_SIZE = 1_048_576;
51
53
  class PayGateServer {
@@ -56,7 +58,10 @@ class PayGateServer {
56
58
  router;
57
59
  server = null;
58
60
  config;
59
- adminKey;
61
+ /** Admin key manager (multiple keys with role-based permissions) */
62
+ adminKeys;
63
+ /** The bootstrap admin key (from constructor or auto-generated) */
64
+ bootstrapAdminKey;
60
65
  stripeHandler = null;
61
66
  /** OAuth 2.1 provider (null if OAuth is not enabled) */
62
67
  oauth = null;
@@ -78,6 +83,8 @@ class PayGateServer {
78
83
  redisSync = null;
79
84
  /** Scoped token manager for short-lived delegated tokens */
80
85
  tokens;
86
+ /** Plugin manager for extensible middleware hooks */
87
+ plugins;
81
88
  /** Server start time (ms since epoch) */
82
89
  startedAt = Date.now();
83
90
  /** Whether the server is draining (shutting down gracefully) */
@@ -90,7 +97,11 @@ class PayGateServer {
90
97
  }
91
98
  constructor(config, adminKey, statePath, remoteUrl, stripeWebhookSecret, servers, redisUrl) {
92
99
  this.config = { ...types_1.DEFAULT_CONFIG, ...config };
93
- this.adminKey = adminKey || `admin_${require('crypto').randomBytes(16).toString('hex')}`;
100
+ this.bootstrapAdminKey = adminKey || `admin_${require('crypto').randomBytes(16).toString('hex')}`;
101
+ // Admin key manager with file persistence (separate from API key state)
102
+ const adminStatePath = statePath ? statePath.replace(/\.json$/, '-admin.json') : undefined;
103
+ this.adminKeys = new admin_keys_1.AdminKeyManager(adminStatePath);
104
+ this.adminKeys.bootstrap(this.bootstrapAdminKey);
94
105
  this.gate = new gate_1.Gate(this.config, statePath);
95
106
  // Multi-server mode: use Router
96
107
  if (servers && servers.length > 0) {
@@ -136,6 +147,9 @@ class PayGateServer {
136
147
  this.metrics.registerGauge('paygate_total_credits_available', 'Total credits across all active keys', () => {
137
148
  return this.gate.store.listKeys().filter(k => k.active).reduce((sum, k) => sum + k.credits, 0);
138
149
  });
150
+ this.metrics.registerGauge('paygate_admin_keys_total', 'Number of active admin keys', () => {
151
+ return this.adminKeys.activeCount;
152
+ });
139
153
  // Analytics engine
140
154
  this.analytics = new analytics_1.AnalyticsEngine();
141
155
  // Alert engine
@@ -175,10 +189,16 @@ class PayGateServer {
175
189
  this.redisSync.atomicTopup(apiKey, amount).catch(() => { });
176
190
  }
177
191
  };
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');
192
+ // Plugin manager for extensible middleware hooks
193
+ this.plugins = new plugin_1.PluginManager();
194
+ this.gate.pluginManager = this.plugins;
195
+ this.metrics.registerGauge('paygate_plugins_total', 'Number of registered plugins', () => {
196
+ return this.plugins.count;
197
+ });
198
+ // Scoped token manager (uses bootstrap admin key as signing secret, padded to min length)
199
+ const tokenSecret = this.bootstrapAdminKey.length >= 8
200
+ ? this.bootstrapAdminKey
201
+ : this.bootstrapAdminKey + require('crypto').randomBytes(8).toString('hex');
182
202
  this.tokens = new tokens_1.ScopedTokenManager(tokenSecret);
183
203
  // Redis distributed state (if configured)
184
204
  if (redisUrl) {
@@ -201,6 +221,22 @@ class PayGateServer {
201
221
  };
202
222
  }
203
223
  }
224
+ /**
225
+ * Register a plugin for extensible middleware hooks.
226
+ * Plugins run in registration order.
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * server.use({
231
+ * name: 'custom-pricing',
232
+ * transformPrice: (tool, base) => tool.startsWith('premium_') ? base * 5 : null,
233
+ * });
234
+ * ```
235
+ */
236
+ use(plugin) {
237
+ this.plugins.register(plugin);
238
+ return this;
239
+ }
204
240
  async start() {
205
241
  // Initialize Redis sync before starting (loads state from Redis + starts pub/sub)
206
242
  if (this.redisSync) {
@@ -209,6 +245,10 @@ class PayGateServer {
209
245
  console.log('[paygate] Redis distributed state enabled');
210
246
  }
211
247
  await this.handler.start();
248
+ // Plugin lifecycle: onStart
249
+ if (this.plugins.count > 0) {
250
+ await this.plugins.executeStart();
251
+ }
212
252
  return new Promise((resolve, reject) => {
213
253
  this.server = (0, http_1.createServer)(async (req, res) => {
214
254
  try {
@@ -222,7 +262,7 @@ class PayGateServer {
222
262
  this.server.listen(this.config.port, () => {
223
263
  const addr = this.server.address();
224
264
  const actualPort = typeof addr === 'object' && addr ? addr.port : this.config.port;
225
- resolve({ port: actualPort, adminKey: this.adminKey });
265
+ resolve({ port: actualPort, adminKey: this.bootstrapAdminKey });
226
266
  });
227
267
  this.server.on('error', reject);
228
268
  });
@@ -238,6 +278,12 @@ class PayGateServer {
238
278
  res.end();
239
279
  return;
240
280
  }
281
+ // Plugin: onRequest — let plugins handle custom endpoints before routing
282
+ if (this.plugins.count > 0) {
283
+ const handled = await this.plugins.executeOnRequest(req, res);
284
+ if (handled)
285
+ return;
286
+ }
241
287
  const url = req.url?.split('?')[0] || '/';
242
288
  switch (url) {
243
289
  case '/mcp':
@@ -346,6 +392,18 @@ class PayGateServer {
346
392
  return this.handleRevokeToken(req, res);
347
393
  case '/tokens/revoked':
348
394
  return this.handleListRevokedTokens(req, res);
395
+ // ─── Admin key management endpoints ──────────────────────────────
396
+ case '/admin/keys':
397
+ if (req.method === 'POST')
398
+ return this.handleCreateAdminKey(req, res);
399
+ if (req.method === 'GET')
400
+ return this.handleListAdminKeys(req, res);
401
+ break;
402
+ case '/admin/keys/revoke':
403
+ return this.handleRevokeAdminKey(req, res);
404
+ // ─── Plugin endpoints ──────────────────────────────────────────────
405
+ case '/plugins':
406
+ return this.handleListPlugins(req, res);
349
407
  // ─── OAuth 2.1 endpoints ─────────────────────────────────────────
350
408
  case '/.well-known/oauth-authorization-server':
351
409
  return this.handleOAuthMetadata(req, res);
@@ -468,7 +526,22 @@ class PayGateServer {
468
526
  }
469
527
  return;
470
528
  }
471
- const response = await this.handler.handleRequest(request, apiKey, clientIp, scopedTokenTools);
529
+ // Plugin: beforeToolCall let plugins modify the request before forwarding
530
+ let pluginRequest = request;
531
+ if (this.plugins.count > 0 && request.method === 'tools/call') {
532
+ const toolName = request.params?.name || '';
533
+ const toolArgs = request.params?.arguments;
534
+ const pluginCtx = { apiKey, toolName, toolArgs, request };
535
+ pluginRequest = await this.plugins.executeBeforeToolCall(pluginCtx);
536
+ }
537
+ let response = await this.handler.handleRequest(pluginRequest, apiKey, clientIp, scopedTokenTools);
538
+ // Plugin: afterToolCall — let plugins modify the response
539
+ if (this.plugins.count > 0 && request.method === 'tools/call') {
540
+ const toolName = request.params?.name || '';
541
+ const toolArgs = request.params?.arguments;
542
+ const pluginCtx = { apiKey, toolName, toolArgs, request };
543
+ response = await this.plugins.executeAfterToolCall(pluginCtx, response);
544
+ }
472
545
  // Inject pricing metadata into tools/list responses
473
546
  if (request.method === 'tools/list' && response.result) {
474
547
  const result = response.result;
@@ -737,6 +810,10 @@ class PayGateServer {
737
810
  createToken: 'POST /tokens — Create scoped token (requires X-Admin-Key)',
738
811
  revokeToken: 'POST /tokens/revoke — Revoke a scoped token (requires X-Admin-Key)',
739
812
  listRevokedTokens: 'GET /tokens/revoked — List revoked tokens (requires X-Admin-Key)',
813
+ adminKeys: 'GET /admin/keys — List admin keys (requires X-Admin-Key, super_admin)',
814
+ createAdminKey: 'POST /admin/keys — Create admin key with role (requires X-Admin-Key, super_admin)',
815
+ revokeAdminKey: 'POST /admin/keys/revoke — Revoke an admin key (requires X-Admin-Key, super_admin)',
816
+ plugins: 'GET /plugins — List registered plugins (requires X-Admin-Key)',
740
817
  ...(this.oauth ? {
741
818
  oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
742
819
  oauthRegister: 'POST /oauth/register — Register OAuth client',
@@ -798,7 +875,7 @@ class PayGateServer {
798
875
  }
799
876
  // ─── /keys — Create ─────────────────────────────────────────────────────────
800
877
  async handleCreateKey(req, res) {
801
- if (!this.checkAdmin(req, res))
878
+ if (!this.checkAdmin(req, res, 'admin'))
802
879
  return;
803
880
  const body = await this.readBody(req);
804
881
  let params;
@@ -891,7 +968,7 @@ class PayGateServer {
891
968
  res.end(JSON.stringify({ error: 'Method not allowed' }));
892
969
  return;
893
970
  }
894
- if (!this.checkAdmin(req, res))
971
+ if (!this.checkAdmin(req, res, 'admin'))
895
972
  return;
896
973
  const body = await this.readBody(req);
897
974
  let params;
@@ -946,7 +1023,7 @@ class PayGateServer {
946
1023
  res.end(JSON.stringify({ error: 'Method not allowed' }));
947
1024
  return;
948
1025
  }
949
- if (!this.checkAdmin(req, res))
1026
+ if (!this.checkAdmin(req, res, 'admin'))
950
1027
  return;
951
1028
  const body = await this.readBody(req);
952
1029
  let params;
@@ -992,7 +1069,7 @@ class PayGateServer {
992
1069
  res.end(JSON.stringify({ error: 'Method not allowed' }));
993
1070
  return;
994
1071
  }
995
- if (!this.checkAdmin(req, res))
1072
+ if (!this.checkAdmin(req, res, 'admin'))
996
1073
  return;
997
1074
  const body = await this.readBody(req);
998
1075
  let params;
@@ -1044,7 +1121,7 @@ class PayGateServer {
1044
1121
  res.end(JSON.stringify({ error: 'Method not allowed' }));
1045
1122
  return;
1046
1123
  }
1047
- if (!this.checkAdmin(req, res))
1124
+ if (!this.checkAdmin(req, res, 'admin'))
1048
1125
  return;
1049
1126
  const body = await this.readBody(req);
1050
1127
  let params;
@@ -1088,7 +1165,7 @@ class PayGateServer {
1088
1165
  res.end(JSON.stringify({ error: 'Method not allowed' }));
1089
1166
  return;
1090
1167
  }
1091
- if (!this.checkAdmin(req, res))
1168
+ if (!this.checkAdmin(req, res, 'admin'))
1092
1169
  return;
1093
1170
  const body = await this.readBody(req);
1094
1171
  let params;
@@ -1138,7 +1215,7 @@ class PayGateServer {
1138
1215
  res.end(JSON.stringify({ error: 'Method not allowed' }));
1139
1216
  return;
1140
1217
  }
1141
- if (!this.checkAdmin(req, res))
1218
+ if (!this.checkAdmin(req, res, 'admin'))
1142
1219
  return;
1143
1220
  const body = await this.readBody(req);
1144
1221
  let params;
@@ -1194,7 +1271,7 @@ class PayGateServer {
1194
1271
  res.end(JSON.stringify({ error: 'Method not allowed' }));
1195
1272
  return;
1196
1273
  }
1197
- if (!this.checkAdmin(req, res))
1274
+ if (!this.checkAdmin(req, res, 'admin'))
1198
1275
  return;
1199
1276
  const body = await this.readBody(req);
1200
1277
  let params;
@@ -1241,7 +1318,7 @@ class PayGateServer {
1241
1318
  res.end(JSON.stringify({ error: 'Method not allowed' }));
1242
1319
  return;
1243
1320
  }
1244
- if (!this.checkAdmin(req, res))
1321
+ if (!this.checkAdmin(req, res, 'admin'))
1245
1322
  return;
1246
1323
  const body = await this.readBody(req);
1247
1324
  let params;
@@ -1316,7 +1393,7 @@ class PayGateServer {
1316
1393
  res.end(JSON.stringify({ error: 'Method not allowed' }));
1317
1394
  return;
1318
1395
  }
1319
- if (!this.checkAdmin(req, res))
1396
+ if (!this.checkAdmin(req, res, 'admin'))
1320
1397
  return;
1321
1398
  const body = await this.readBody(req);
1322
1399
  let params;
@@ -1437,7 +1514,7 @@ class PayGateServer {
1437
1514
  res.end(JSON.stringify({ error: 'Method not allowed' }));
1438
1515
  return;
1439
1516
  }
1440
- if (!this.checkAdmin(req, res))
1517
+ if (!this.checkAdmin(req, res, 'admin'))
1441
1518
  return;
1442
1519
  const body = await this.readBody(req);
1443
1520
  let params;
@@ -1882,7 +1959,7 @@ class PayGateServer {
1882
1959
  }));
1883
1960
  }
1884
1961
  async handleConfigureAlerts(req, res) {
1885
- if (!this.checkAdmin(req, res))
1962
+ if (!this.checkAdmin(req, res, 'admin'))
1886
1963
  return;
1887
1964
  const body = await this.readBody(req);
1888
1965
  let params;
@@ -1942,7 +2019,7 @@ class PayGateServer {
1942
2019
  }));
1943
2020
  }
1944
2021
  handleClearDeadLetters(req, res) {
1945
- if (!this.checkAdmin(req, res))
2022
+ if (!this.checkAdmin(req, res, 'admin'))
1946
2023
  return;
1947
2024
  if (!this.gate.webhook) {
1948
2025
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -2048,9 +2125,10 @@ class PayGateServer {
2048
2125
  res.end(JSON.stringify(this.audit.stats(), null, 2));
2049
2126
  }
2050
2127
  // ─── Helpers ────────────────────────────────────────────────────────────────
2051
- checkAdmin(req, res) {
2128
+ checkAdmin(req, res, minRole) {
2052
2129
  const adminKey = req.headers['x-admin-key'];
2053
- if (adminKey !== this.adminKey) {
2130
+ const record = adminKey ? this.adminKeys.validate(adminKey) : null;
2131
+ if (!record) {
2054
2132
  this.audit.log('admin.auth_failed', 'unknown', `Admin auth failed on ${req.url}`, {
2055
2133
  url: req.url,
2056
2134
  method: req.method,
@@ -2059,6 +2137,18 @@ class PayGateServer {
2059
2137
  res.end(JSON.stringify({ error: 'Invalid admin key' }));
2060
2138
  return false;
2061
2139
  }
2140
+ // Role-based permission check (if a minimum role is specified)
2141
+ if (minRole && admin_keys_1.ROLE_HIERARCHY[record.role] < admin_keys_1.ROLE_HIERARCHY[minRole]) {
2142
+ this.audit.log('admin.auth_failed', adminKey.slice(0, 7) + '...' + adminKey.slice(-4), `Insufficient role for ${req.url} (need ${minRole}, have ${record.role})`, {
2143
+ url: req.url,
2144
+ method: req.method,
2145
+ requiredRole: minRole,
2146
+ currentRole: record.role,
2147
+ });
2148
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2149
+ res.end(JSON.stringify({ error: 'Insufficient permissions', requiredRole: minRole, currentRole: record.role }));
2150
+ return false;
2151
+ }
2062
2152
  return true;
2063
2153
  }
2064
2154
  // ─── /teams — Team management ────────────────────────────────────────────
@@ -2078,7 +2168,7 @@ class PayGateServer {
2078
2168
  res.end(JSON.stringify({ teams, count: teams.length }));
2079
2169
  }
2080
2170
  async handleCreateTeam(req, res) {
2081
- if (!this.checkAdmin(req, res))
2171
+ if (!this.checkAdmin(req, res, 'admin'))
2082
2172
  return;
2083
2173
  if (req.method !== 'POST') {
2084
2174
  res.writeHead(405, { 'Content-Type': 'application/json' });
@@ -2117,7 +2207,7 @@ class PayGateServer {
2117
2207
  res.end(JSON.stringify({ message: 'Team created', team }));
2118
2208
  }
2119
2209
  async handleUpdateTeam(req, res) {
2120
- if (!this.checkAdmin(req, res))
2210
+ if (!this.checkAdmin(req, res, 'admin'))
2121
2211
  return;
2122
2212
  if (req.method !== 'POST') {
2123
2213
  res.writeHead(405, { 'Content-Type': 'application/json' });
@@ -2157,7 +2247,7 @@ class PayGateServer {
2157
2247
  res.end(JSON.stringify({ message: 'Team updated', team: this.teams.getTeam(params.teamId) }));
2158
2248
  }
2159
2249
  async handleDeleteTeam(req, res) {
2160
- if (!this.checkAdmin(req, res))
2250
+ if (!this.checkAdmin(req, res, 'admin'))
2161
2251
  return;
2162
2252
  if (req.method !== 'POST') {
2163
2253
  res.writeHead(405, { 'Content-Type': 'application/json' });
@@ -2191,7 +2281,7 @@ class PayGateServer {
2191
2281
  res.end(JSON.stringify({ message: 'Team deleted' }));
2192
2282
  }
2193
2283
  async handleTeamAssignKey(req, res) {
2194
- if (!this.checkAdmin(req, res))
2284
+ if (!this.checkAdmin(req, res, 'admin'))
2195
2285
  return;
2196
2286
  if (req.method !== 'POST') {
2197
2287
  res.writeHead(405, { 'Content-Type': 'application/json' });
@@ -2240,7 +2330,7 @@ class PayGateServer {
2240
2330
  res.end(JSON.stringify({ message: 'Key assigned to team' }));
2241
2331
  }
2242
2332
  async handleTeamRemoveKey(req, res) {
2243
- if (!this.checkAdmin(req, res))
2333
+ if (!this.checkAdmin(req, res, 'admin'))
2244
2334
  return;
2245
2335
  if (req.method !== 'POST') {
2246
2336
  res.writeHead(405, { 'Content-Type': 'application/json' });
@@ -2320,7 +2410,7 @@ class PayGateServer {
2320
2410
  }
2321
2411
  // ─── /tokens — Create scoped token ──────────────────────────────────────────
2322
2412
  async handleCreateToken(req, res) {
2323
- if (!this.checkAdmin(req, res))
2413
+ if (!this.checkAdmin(req, res, 'admin'))
2324
2414
  return;
2325
2415
  const body = await this.readBody(req);
2326
2416
  let params;
@@ -2375,7 +2465,7 @@ class PayGateServer {
2375
2465
  res.end(JSON.stringify({ error: 'Method not allowed' }));
2376
2466
  return;
2377
2467
  }
2378
- if (!this.checkAdmin(req, res))
2468
+ if (!this.checkAdmin(req, res, 'admin'))
2379
2469
  return;
2380
2470
  const body = await this.readBody(req);
2381
2471
  let params;
@@ -2447,6 +2537,138 @@ class PayGateServer {
2447
2537
  })),
2448
2538
  }));
2449
2539
  }
2540
+ // ─── /admin/keys — Admin key management ────────────────────────────────────
2541
+ // ─── GET /plugins — List registered plugins ─────────────────────────────
2542
+ handleListPlugins(req, res) {
2543
+ if (req.method !== 'GET') {
2544
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2545
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
2546
+ return;
2547
+ }
2548
+ if (!this.checkAdmin(req, res))
2549
+ return;
2550
+ const plugins = this.plugins.list();
2551
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2552
+ res.end(JSON.stringify({ count: plugins.length, plugins }));
2553
+ }
2554
+ // ─── Admin Key Management ────────────────────────────────────────────────
2555
+ handleListAdminKeys(req, res) {
2556
+ if (req.method !== 'GET') {
2557
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2558
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
2559
+ return;
2560
+ }
2561
+ if (!this.checkAdmin(req, res, 'super_admin'))
2562
+ return;
2563
+ const keys = this.adminKeys.list().map(k => ({
2564
+ key: k.key.slice(0, 7) + '...' + k.key.slice(-4),
2565
+ name: k.name,
2566
+ role: k.role,
2567
+ createdAt: k.createdAt,
2568
+ createdBy: k.createdBy,
2569
+ active: k.active,
2570
+ lastUsedAt: k.lastUsedAt,
2571
+ }));
2572
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2573
+ res.end(JSON.stringify({ count: keys.length, keys }));
2574
+ }
2575
+ async handleCreateAdminKey(req, res) {
2576
+ if (req.method !== 'POST') {
2577
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2578
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
2579
+ return;
2580
+ }
2581
+ if (!this.checkAdmin(req, res, 'super_admin'))
2582
+ return;
2583
+ const body = await this.readBody(req);
2584
+ let params;
2585
+ try {
2586
+ params = JSON.parse(body);
2587
+ }
2588
+ catch {
2589
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2590
+ res.end(JSON.stringify({ error: 'Invalid JSON body' }));
2591
+ return;
2592
+ }
2593
+ if (!params.name || typeof params.name !== 'string') {
2594
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2595
+ res.end(JSON.stringify({ error: 'Missing required field: name' }));
2596
+ return;
2597
+ }
2598
+ const role = (params.role || 'admin');
2599
+ if (!['super_admin', 'admin', 'viewer'].includes(role)) {
2600
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2601
+ res.end(JSON.stringify({ error: 'Invalid role. Must be super_admin, admin, or viewer.' }));
2602
+ return;
2603
+ }
2604
+ // Mask the requesting admin key for audit
2605
+ const callerKey = req.headers['x-admin-key'];
2606
+ const callerMasked = callerKey.slice(0, 7) + '...' + callerKey.slice(-4);
2607
+ const record = this.adminKeys.create(params.name, role, callerMasked);
2608
+ this.audit.log('admin_key.created', callerMasked, `Created admin key "${params.name}" with role ${role}`, {
2609
+ newKeyMasked: record.key.slice(0, 7) + '...' + record.key.slice(-4),
2610
+ role,
2611
+ });
2612
+ this.gate.webhook?.emitAdmin('admin_key.created', callerMasked, {
2613
+ newKeyMasked: record.key.slice(0, 7) + '...' + record.key.slice(-4),
2614
+ name: params.name,
2615
+ role,
2616
+ });
2617
+ res.writeHead(201, { 'Content-Type': 'application/json' });
2618
+ res.end(JSON.stringify({
2619
+ key: record.key,
2620
+ name: record.name,
2621
+ role: record.role,
2622
+ createdAt: record.createdAt,
2623
+ }));
2624
+ }
2625
+ async handleRevokeAdminKey(req, res) {
2626
+ if (req.method !== 'POST') {
2627
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2628
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
2629
+ return;
2630
+ }
2631
+ if (!this.checkAdmin(req, res, 'super_admin'))
2632
+ return;
2633
+ const body = await this.readBody(req);
2634
+ let params;
2635
+ try {
2636
+ params = JSON.parse(body);
2637
+ }
2638
+ catch {
2639
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2640
+ res.end(JSON.stringify({ error: 'Invalid JSON body' }));
2641
+ return;
2642
+ }
2643
+ if (!params.key || typeof params.key !== 'string') {
2644
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2645
+ res.end(JSON.stringify({ error: 'Missing required field: key' }));
2646
+ return;
2647
+ }
2648
+ const callerKey = req.headers['x-admin-key'];
2649
+ // Prevent revoking your own key
2650
+ if (params.key === callerKey) {
2651
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2652
+ res.end(JSON.stringify({ error: 'Cannot revoke your own admin key' }));
2653
+ return;
2654
+ }
2655
+ const result = this.adminKeys.revoke(params.key);
2656
+ if (!result.success) {
2657
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2658
+ res.end(JSON.stringify({ error: result.error }));
2659
+ return;
2660
+ }
2661
+ const callerMasked = callerKey.slice(0, 7) + '...' + callerKey.slice(-4);
2662
+ const targetMasked = params.key.slice(0, 7) + '...' + params.key.slice(-4);
2663
+ this.audit.log('admin_key.revoked', callerMasked, `Revoked admin key ${targetMasked}`, {
2664
+ revokedKeyMasked: targetMasked,
2665
+ });
2666
+ this.gate.webhook?.emitAdmin('admin_key.revoked', callerMasked, {
2667
+ revokedKeyMasked: targetMasked,
2668
+ });
2669
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2670
+ res.end(JSON.stringify({ revoked: true }));
2671
+ }
2450
2672
  /**
2451
2673
  * Sync a key mutation to Redis. Call after any local KeyStore mutation
2452
2674
  * (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
@@ -2480,6 +2702,10 @@ class PayGateServer {
2480
2702
  });
2481
2703
  }
2482
2704
  async stop() {
2705
+ // Plugin lifecycle: onStop (reverse order)
2706
+ if (this.plugins.count > 0) {
2707
+ await this.plugins.executeStop();
2708
+ }
2483
2709
  await this.handler.stop();
2484
2710
  this.gate.destroy();
2485
2711
  this.oauth?.destroy();
@@ -2530,6 +2756,10 @@ class PayGateServer {
2530
2756
  check();
2531
2757
  });
2532
2758
  console.log('[paygate] Drained — tearing down resources');
2759
+ // Plugin lifecycle: onStop (reverse order)
2760
+ if (this.plugins.count > 0) {
2761
+ await this.plugins.executeStop();
2762
+ }
2533
2763
  // Tear down resources (but skip server.close, already closed above)
2534
2764
  await this.handler.stop();
2535
2765
  this.gate.destroy();