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/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') {