paygate-mcp 3.3.0 → 3.5.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 +191 -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 +12 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +88 -15
- package/dist/gate.js.map +1 -1
- package/dist/groups.d.ts +140 -0
- package/dist/groups.d.ts.map +1 -0
- package/dist/groups.js +258 -0
- package/dist/groups.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +172 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +244 -0
- package/dist/plugin.js.map +1 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +325 -1
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +5 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +17 -0
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -47,6 +47,8 @@ const redis_client_1 = require("./redis-client");
|
|
|
47
47
|
const redis_sync_1 = require("./redis-sync");
|
|
48
48
|
const tokens_1 = require("./tokens");
|
|
49
49
|
const admin_keys_1 = require("./admin-keys");
|
|
50
|
+
const plugin_1 = require("./plugin");
|
|
51
|
+
const groups_1 = require("./groups");
|
|
50
52
|
/** Max request body size: 1MB */
|
|
51
53
|
const MAX_BODY_SIZE = 1_048_576;
|
|
52
54
|
class PayGateServer {
|
|
@@ -82,6 +84,9 @@ class PayGateServer {
|
|
|
82
84
|
redisSync = null;
|
|
83
85
|
/** Scoped token manager for short-lived delegated tokens */
|
|
84
86
|
tokens;
|
|
87
|
+
/** Plugin manager for extensible middleware hooks */
|
|
88
|
+
plugins;
|
|
89
|
+
groups;
|
|
85
90
|
/** Server start time (ms since epoch) */
|
|
86
91
|
startedAt = Date.now();
|
|
87
92
|
/** Whether the server is draining (shutting down gracefully) */
|
|
@@ -186,6 +191,17 @@ class PayGateServer {
|
|
|
186
191
|
this.redisSync.atomicTopup(apiKey, amount).catch(() => { });
|
|
187
192
|
}
|
|
188
193
|
};
|
|
194
|
+
// Plugin manager for extensible middleware hooks
|
|
195
|
+
this.plugins = new plugin_1.PluginManager();
|
|
196
|
+
this.gate.pluginManager = this.plugins;
|
|
197
|
+
this.groups = new groups_1.KeyGroupManager();
|
|
198
|
+
this.gate.groupManager = this.groups;
|
|
199
|
+
this.metrics.registerGauge('paygate_plugins_total', 'Number of registered plugins', () => {
|
|
200
|
+
return this.plugins.count;
|
|
201
|
+
});
|
|
202
|
+
this.metrics.registerGauge('paygate_groups_total', 'Number of active key groups', () => {
|
|
203
|
+
return this.groups.count;
|
|
204
|
+
});
|
|
189
205
|
// Scoped token manager (uses bootstrap admin key as signing secret, padded to min length)
|
|
190
206
|
const tokenSecret = this.bootstrapAdminKey.length >= 8
|
|
191
207
|
? this.bootstrapAdminKey
|
|
@@ -212,6 +228,22 @@ class PayGateServer {
|
|
|
212
228
|
};
|
|
213
229
|
}
|
|
214
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Register a plugin for extensible middleware hooks.
|
|
233
|
+
* Plugins run in registration order.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* server.use({
|
|
238
|
+
* name: 'custom-pricing',
|
|
239
|
+
* transformPrice: (tool, base) => tool.startsWith('premium_') ? base * 5 : null,
|
|
240
|
+
* });
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
use(plugin) {
|
|
244
|
+
this.plugins.register(plugin);
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
215
247
|
async start() {
|
|
216
248
|
// Initialize Redis sync before starting (loads state from Redis + starts pub/sub)
|
|
217
249
|
if (this.redisSync) {
|
|
@@ -220,6 +252,10 @@ class PayGateServer {
|
|
|
220
252
|
console.log('[paygate] Redis distributed state enabled');
|
|
221
253
|
}
|
|
222
254
|
await this.handler.start();
|
|
255
|
+
// Plugin lifecycle: onStart
|
|
256
|
+
if (this.plugins.count > 0) {
|
|
257
|
+
await this.plugins.executeStart();
|
|
258
|
+
}
|
|
223
259
|
return new Promise((resolve, reject) => {
|
|
224
260
|
this.server = (0, http_1.createServer)(async (req, res) => {
|
|
225
261
|
try {
|
|
@@ -249,6 +285,12 @@ class PayGateServer {
|
|
|
249
285
|
res.end();
|
|
250
286
|
return;
|
|
251
287
|
}
|
|
288
|
+
// Plugin: onRequest — let plugins handle custom endpoints before routing
|
|
289
|
+
if (this.plugins.count > 0) {
|
|
290
|
+
const handled = await this.plugins.executeOnRequest(req, res);
|
|
291
|
+
if (handled)
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
252
294
|
const url = req.url?.split('?')[0] || '/';
|
|
253
295
|
switch (url) {
|
|
254
296
|
case '/mcp':
|
|
@@ -366,6 +408,23 @@ class PayGateServer {
|
|
|
366
408
|
break;
|
|
367
409
|
case '/admin/keys/revoke':
|
|
368
410
|
return this.handleRevokeAdminKey(req, res);
|
|
411
|
+
// ─── Plugin endpoints ──────────────────────────────────────────────
|
|
412
|
+
case '/plugins':
|
|
413
|
+
return this.handleListPlugins(req, res);
|
|
414
|
+
case '/groups':
|
|
415
|
+
if (req.method === 'GET')
|
|
416
|
+
return this.handleListGroups(req, res);
|
|
417
|
+
if (req.method === 'POST')
|
|
418
|
+
return this.handleCreateGroup(req, res);
|
|
419
|
+
break;
|
|
420
|
+
case '/groups/update':
|
|
421
|
+
return this.handleUpdateGroup(req, res);
|
|
422
|
+
case '/groups/delete':
|
|
423
|
+
return this.handleDeleteGroup(req, res);
|
|
424
|
+
case '/groups/assign':
|
|
425
|
+
return this.handleAssignKeyToGroup(req, res);
|
|
426
|
+
case '/groups/remove':
|
|
427
|
+
return this.handleRemoveKeyFromGroup(req, res);
|
|
369
428
|
// ─── OAuth 2.1 endpoints ─────────────────────────────────────────
|
|
370
429
|
case '/.well-known/oauth-authorization-server':
|
|
371
430
|
return this.handleOAuthMetadata(req, res);
|
|
@@ -488,7 +547,22 @@ class PayGateServer {
|
|
|
488
547
|
}
|
|
489
548
|
return;
|
|
490
549
|
}
|
|
491
|
-
|
|
550
|
+
// Plugin: beforeToolCall — let plugins modify the request before forwarding
|
|
551
|
+
let pluginRequest = request;
|
|
552
|
+
if (this.plugins.count > 0 && request.method === 'tools/call') {
|
|
553
|
+
const toolName = request.params?.name || '';
|
|
554
|
+
const toolArgs = request.params?.arguments;
|
|
555
|
+
const pluginCtx = { apiKey, toolName, toolArgs, request };
|
|
556
|
+
pluginRequest = await this.plugins.executeBeforeToolCall(pluginCtx);
|
|
557
|
+
}
|
|
558
|
+
let response = await this.handler.handleRequest(pluginRequest, apiKey, clientIp, scopedTokenTools);
|
|
559
|
+
// Plugin: afterToolCall — let plugins modify the response
|
|
560
|
+
if (this.plugins.count > 0 && request.method === 'tools/call') {
|
|
561
|
+
const toolName = request.params?.name || '';
|
|
562
|
+
const toolArgs = request.params?.arguments;
|
|
563
|
+
const pluginCtx = { apiKey, toolName, toolArgs, request };
|
|
564
|
+
response = await this.plugins.executeAfterToolCall(pluginCtx, response);
|
|
565
|
+
}
|
|
492
566
|
// Inject pricing metadata into tools/list responses
|
|
493
567
|
if (request.method === 'tools/list' && response.result) {
|
|
494
568
|
const result = response.result;
|
|
@@ -760,6 +834,13 @@ class PayGateServer {
|
|
|
760
834
|
adminKeys: 'GET /admin/keys — List admin keys (requires X-Admin-Key, super_admin)',
|
|
761
835
|
createAdminKey: 'POST /admin/keys — Create admin key with role (requires X-Admin-Key, super_admin)',
|
|
762
836
|
revokeAdminKey: 'POST /admin/keys/revoke — Revoke an admin key (requires X-Admin-Key, super_admin)',
|
|
837
|
+
plugins: 'GET /plugins — List registered plugins (requires X-Admin-Key)',
|
|
838
|
+
listGroups: 'GET /groups — List key groups (requires X-Admin-Key)',
|
|
839
|
+
createGroup: 'POST /groups — Create a key group (requires X-Admin-Key)',
|
|
840
|
+
updateGroup: 'POST /groups/update — Update group settings (requires X-Admin-Key)',
|
|
841
|
+
deleteGroup: 'POST /groups/delete — Delete a key group (requires X-Admin-Key)',
|
|
842
|
+
assignKeyToGroup: 'POST /groups/assign — Assign a key to a group (requires X-Admin-Key)',
|
|
843
|
+
removeKeyFromGroup: 'POST /groups/remove — Remove a key from a group (requires X-Admin-Key)',
|
|
763
844
|
...(this.oauth ? {
|
|
764
845
|
oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
|
|
765
846
|
oauthRegister: 'POST /oauth/register — Register OAuth client',
|
|
@@ -2484,6 +2565,241 @@ class PayGateServer {
|
|
|
2484
2565
|
}));
|
|
2485
2566
|
}
|
|
2486
2567
|
// ─── /admin/keys — Admin key management ────────────────────────────────────
|
|
2568
|
+
// ─── GET /plugins — List registered plugins ─────────────────────────────
|
|
2569
|
+
handleListPlugins(req, res) {
|
|
2570
|
+
if (req.method !== 'GET') {
|
|
2571
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
2572
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (!this.checkAdmin(req, res))
|
|
2576
|
+
return;
|
|
2577
|
+
const plugins = this.plugins.list();
|
|
2578
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2579
|
+
res.end(JSON.stringify({ count: plugins.length, plugins }));
|
|
2580
|
+
}
|
|
2581
|
+
// ─── Key Group Endpoints ─────────────────────────────────────────────────
|
|
2582
|
+
handleListGroups(req, res) {
|
|
2583
|
+
if (!this.checkAdmin(req, res))
|
|
2584
|
+
return;
|
|
2585
|
+
const groups = this.groups.listGroups();
|
|
2586
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2587
|
+
res.end(JSON.stringify({ count: groups.length, groups }));
|
|
2588
|
+
}
|
|
2589
|
+
async handleCreateGroup(req, res) {
|
|
2590
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2591
|
+
return;
|
|
2592
|
+
const body = await this.readBody(req);
|
|
2593
|
+
let params;
|
|
2594
|
+
try {
|
|
2595
|
+
params = JSON.parse(body);
|
|
2596
|
+
}
|
|
2597
|
+
catch {
|
|
2598
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2599
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
try {
|
|
2603
|
+
const group = this.groups.createGroup({
|
|
2604
|
+
name: String(params.name || ''),
|
|
2605
|
+
description: params.description,
|
|
2606
|
+
allowedTools: params.allowedTools,
|
|
2607
|
+
deniedTools: params.deniedTools,
|
|
2608
|
+
rateLimitPerMin: params.rateLimitPerMin,
|
|
2609
|
+
toolPricing: params.toolPricing,
|
|
2610
|
+
quota: params.quota ? {
|
|
2611
|
+
dailyCallLimit: Math.max(0, Math.floor(Number(params.quota.dailyCallLimit) || 0)),
|
|
2612
|
+
monthlyCallLimit: Math.max(0, Math.floor(Number(params.quota.monthlyCallLimit) || 0)),
|
|
2613
|
+
dailyCreditLimit: Math.max(0, Math.floor(Number(params.quota.dailyCreditLimit) || 0)),
|
|
2614
|
+
monthlyCreditLimit: Math.max(0, Math.floor(Number(params.quota.monthlyCreditLimit) || 0)),
|
|
2615
|
+
} : undefined,
|
|
2616
|
+
ipAllowlist: params.ipAllowlist,
|
|
2617
|
+
defaultCredits: params.defaultCredits,
|
|
2618
|
+
maxSpendingLimit: params.maxSpendingLimit,
|
|
2619
|
+
tags: params.tags,
|
|
2620
|
+
});
|
|
2621
|
+
this.audit.log('group.created', 'admin', `Group created: ${group.name}`, { groupId: group.id, name: group.name });
|
|
2622
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
2623
|
+
res.end(JSON.stringify(group));
|
|
2624
|
+
}
|
|
2625
|
+
catch (err) {
|
|
2626
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2627
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
async handleUpdateGroup(req, res) {
|
|
2631
|
+
if (req.method !== 'POST') {
|
|
2632
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
2633
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2637
|
+
return;
|
|
2638
|
+
const body = await this.readBody(req);
|
|
2639
|
+
let params;
|
|
2640
|
+
try {
|
|
2641
|
+
params = JSON.parse(body);
|
|
2642
|
+
}
|
|
2643
|
+
catch {
|
|
2644
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2645
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
const groupId = String(params.id || '');
|
|
2649
|
+
if (!groupId) {
|
|
2650
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2651
|
+
res.end(JSON.stringify({ error: 'Missing id field' }));
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
const group = this.groups.updateGroup(groupId, {
|
|
2656
|
+
name: params.name,
|
|
2657
|
+
description: params.description,
|
|
2658
|
+
allowedTools: params.allowedTools,
|
|
2659
|
+
deniedTools: params.deniedTools,
|
|
2660
|
+
rateLimitPerMin: params.rateLimitPerMin,
|
|
2661
|
+
toolPricing: params.toolPricing,
|
|
2662
|
+
quota: params.quota === null ? null : params.quota ? {
|
|
2663
|
+
dailyCallLimit: Math.max(0, Math.floor(Number(params.quota.dailyCallLimit) || 0)),
|
|
2664
|
+
monthlyCallLimit: Math.max(0, Math.floor(Number(params.quota.monthlyCallLimit) || 0)),
|
|
2665
|
+
dailyCreditLimit: Math.max(0, Math.floor(Number(params.quota.dailyCreditLimit) || 0)),
|
|
2666
|
+
monthlyCreditLimit: Math.max(0, Math.floor(Number(params.quota.monthlyCreditLimit) || 0)),
|
|
2667
|
+
} : undefined,
|
|
2668
|
+
ipAllowlist: params.ipAllowlist,
|
|
2669
|
+
defaultCredits: params.defaultCredits,
|
|
2670
|
+
maxSpendingLimit: params.maxSpendingLimit,
|
|
2671
|
+
tags: params.tags,
|
|
2672
|
+
});
|
|
2673
|
+
this.audit.log('group.updated', 'admin', `Group updated: ${group.name}`, { groupId: group.id });
|
|
2674
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2675
|
+
res.end(JSON.stringify(group));
|
|
2676
|
+
}
|
|
2677
|
+
catch (err) {
|
|
2678
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2679
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
async handleDeleteGroup(req, res) {
|
|
2683
|
+
if (req.method !== 'POST') {
|
|
2684
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
2685
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2689
|
+
return;
|
|
2690
|
+
const body = await this.readBody(req);
|
|
2691
|
+
let params;
|
|
2692
|
+
try {
|
|
2693
|
+
params = JSON.parse(body);
|
|
2694
|
+
}
|
|
2695
|
+
catch {
|
|
2696
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2697
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
const groupId = String(params.id || '');
|
|
2701
|
+
if (!groupId) {
|
|
2702
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2703
|
+
res.end(JSON.stringify({ error: 'Missing id field' }));
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const deleted = this.groups.deleteGroup(groupId);
|
|
2707
|
+
if (!deleted) {
|
|
2708
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
2709
|
+
res.end(JSON.stringify({ error: 'Group not found' }));
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
this.audit.log('group.deleted', 'admin', `Group deleted: ${groupId}`, { groupId });
|
|
2713
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2714
|
+
res.end(JSON.stringify({ ok: true, message: `Group ${groupId} deleted` }));
|
|
2715
|
+
}
|
|
2716
|
+
async handleAssignKeyToGroup(req, res) {
|
|
2717
|
+
if (req.method !== 'POST') {
|
|
2718
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
2719
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2723
|
+
return;
|
|
2724
|
+
const body = await this.readBody(req);
|
|
2725
|
+
let params;
|
|
2726
|
+
try {
|
|
2727
|
+
params = JSON.parse(body);
|
|
2728
|
+
}
|
|
2729
|
+
catch {
|
|
2730
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2731
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
const apiKey = String(params.key || '');
|
|
2735
|
+
const groupId = String(params.groupId || '');
|
|
2736
|
+
if (!apiKey || !groupId) {
|
|
2737
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2738
|
+
res.end(JSON.stringify({ error: 'Missing key or groupId field' }));
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
// Verify key exists
|
|
2742
|
+
const keyRecord = this.gate.store.getKey(apiKey);
|
|
2743
|
+
if (!keyRecord) {
|
|
2744
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
2745
|
+
res.end(JSON.stringify({ error: 'API key not found' }));
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
try {
|
|
2749
|
+
this.groups.assignKey(apiKey, groupId);
|
|
2750
|
+
// Update key record's group field
|
|
2751
|
+
keyRecord.group = groupId;
|
|
2752
|
+
this.audit.log('group.key_assigned', 'admin', `Key assigned to group ${groupId}`, {
|
|
2753
|
+
keyMasked: (0, audit_1.maskKeyForAudit)(apiKey), groupId,
|
|
2754
|
+
});
|
|
2755
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2756
|
+
res.end(JSON.stringify({ ok: true, message: `Key assigned to group ${groupId}` }));
|
|
2757
|
+
}
|
|
2758
|
+
catch (err) {
|
|
2759
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2760
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
async handleRemoveKeyFromGroup(req, res) {
|
|
2764
|
+
if (req.method !== 'POST') {
|
|
2765
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
2766
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
2770
|
+
return;
|
|
2771
|
+
const body = await this.readBody(req);
|
|
2772
|
+
let params;
|
|
2773
|
+
try {
|
|
2774
|
+
params = JSON.parse(body);
|
|
2775
|
+
}
|
|
2776
|
+
catch {
|
|
2777
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2778
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const apiKey = String(params.key || '');
|
|
2782
|
+
if (!apiKey) {
|
|
2783
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2784
|
+
res.end(JSON.stringify({ error: 'Missing key field' }));
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
const removed = this.groups.removeKey(apiKey);
|
|
2788
|
+
if (!removed) {
|
|
2789
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
2790
|
+
res.end(JSON.stringify({ error: 'Key not in any group' }));
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
// Clear group field on key record
|
|
2794
|
+
const keyRecord = this.gate.store.getKey(apiKey);
|
|
2795
|
+
if (keyRecord) {
|
|
2796
|
+
delete keyRecord.group;
|
|
2797
|
+
}
|
|
2798
|
+
this.audit.log('group.key_removed', 'admin', `Key removed from group`, { keyMasked: (0, audit_1.maskKeyForAudit)(apiKey) });
|
|
2799
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2800
|
+
res.end(JSON.stringify({ ok: true, message: 'Key removed from group' }));
|
|
2801
|
+
}
|
|
2802
|
+
// ─── Admin Key Management ────────────────────────────────────────────────
|
|
2487
2803
|
handleListAdminKeys(req, res) {
|
|
2488
2804
|
if (req.method !== 'GET') {
|
|
2489
2805
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
@@ -2634,6 +2950,10 @@ class PayGateServer {
|
|
|
2634
2950
|
});
|
|
2635
2951
|
}
|
|
2636
2952
|
async stop() {
|
|
2953
|
+
// Plugin lifecycle: onStop (reverse order)
|
|
2954
|
+
if (this.plugins.count > 0) {
|
|
2955
|
+
await this.plugins.executeStop();
|
|
2956
|
+
}
|
|
2637
2957
|
await this.handler.stop();
|
|
2638
2958
|
this.gate.destroy();
|
|
2639
2959
|
this.oauth?.destroy();
|
|
@@ -2684,6 +3004,10 @@ class PayGateServer {
|
|
|
2684
3004
|
check();
|
|
2685
3005
|
});
|
|
2686
3006
|
console.log('[paygate] Drained — tearing down resources');
|
|
3007
|
+
// Plugin lifecycle: onStop (reverse order)
|
|
3008
|
+
if (this.plugins.count > 0) {
|
|
3009
|
+
await this.plugins.executeStop();
|
|
3010
|
+
}
|
|
2687
3011
|
// Tear down resources (but skip server.close, already closed above)
|
|
2688
3012
|
await this.handler.stop();
|
|
2689
3013
|
this.gate.destroy();
|