paygate-mcp 3.4.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 +91 -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 +9 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +53 -11
- 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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +248 -0
- 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
|
@@ -48,6 +48,7 @@ const redis_sync_1 = require("./redis-sync");
|
|
|
48
48
|
const tokens_1 = require("./tokens");
|
|
49
49
|
const admin_keys_1 = require("./admin-keys");
|
|
50
50
|
const plugin_1 = require("./plugin");
|
|
51
|
+
const groups_1 = require("./groups");
|
|
51
52
|
/** Max request body size: 1MB */
|
|
52
53
|
const MAX_BODY_SIZE = 1_048_576;
|
|
53
54
|
class PayGateServer {
|
|
@@ -85,6 +86,7 @@ class PayGateServer {
|
|
|
85
86
|
tokens;
|
|
86
87
|
/** Plugin manager for extensible middleware hooks */
|
|
87
88
|
plugins;
|
|
89
|
+
groups;
|
|
88
90
|
/** Server start time (ms since epoch) */
|
|
89
91
|
startedAt = Date.now();
|
|
90
92
|
/** Whether the server is draining (shutting down gracefully) */
|
|
@@ -192,9 +194,14 @@ class PayGateServer {
|
|
|
192
194
|
// Plugin manager for extensible middleware hooks
|
|
193
195
|
this.plugins = new plugin_1.PluginManager();
|
|
194
196
|
this.gate.pluginManager = this.plugins;
|
|
197
|
+
this.groups = new groups_1.KeyGroupManager();
|
|
198
|
+
this.gate.groupManager = this.groups;
|
|
195
199
|
this.metrics.registerGauge('paygate_plugins_total', 'Number of registered plugins', () => {
|
|
196
200
|
return this.plugins.count;
|
|
197
201
|
});
|
|
202
|
+
this.metrics.registerGauge('paygate_groups_total', 'Number of active key groups', () => {
|
|
203
|
+
return this.groups.count;
|
|
204
|
+
});
|
|
198
205
|
// Scoped token manager (uses bootstrap admin key as signing secret, padded to min length)
|
|
199
206
|
const tokenSecret = this.bootstrapAdminKey.length >= 8
|
|
200
207
|
? this.bootstrapAdminKey
|
|
@@ -404,6 +411,20 @@ class PayGateServer {
|
|
|
404
411
|
// ─── Plugin endpoints ──────────────────────────────────────────────
|
|
405
412
|
case '/plugins':
|
|
406
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);
|
|
407
428
|
// ─── OAuth 2.1 endpoints ─────────────────────────────────────────
|
|
408
429
|
case '/.well-known/oauth-authorization-server':
|
|
409
430
|
return this.handleOAuthMetadata(req, res);
|
|
@@ -814,6 +835,12 @@ class PayGateServer {
|
|
|
814
835
|
createAdminKey: 'POST /admin/keys — Create admin key with role (requires X-Admin-Key, super_admin)',
|
|
815
836
|
revokeAdminKey: 'POST /admin/keys/revoke — Revoke an admin key (requires X-Admin-Key, super_admin)',
|
|
816
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)',
|
|
817
844
|
...(this.oauth ? {
|
|
818
845
|
oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
|
|
819
846
|
oauthRegister: 'POST /oauth/register — Register OAuth client',
|
|
@@ -2551,6 +2578,227 @@ class PayGateServer {
|
|
|
2551
2578
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2552
2579
|
res.end(JSON.stringify({ count: plugins.length, plugins }));
|
|
2553
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
|
+
}
|
|
2554
2802
|
// ─── Admin Key Management ────────────────────────────────────────────────
|
|
2555
2803
|
handleListAdminKeys(req, res) {
|
|
2556
2804
|
if (req.method !== 'GET') {
|