paygate-mcp 3.6.0 → 3.8.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
@@ -159,6 +159,10 @@ export declare class PayGateServer {
159
159
  private handleGetDeadLetters;
160
160
  private handleClearDeadLetters;
161
161
  private handleWebhookStats;
162
+ private handleListWebhookFilters;
163
+ private handleCreateWebhookFilter;
164
+ private handleUpdateWebhookFilter;
165
+ private handleDeleteWebhookFilter;
162
166
  private handleAudit;
163
167
  private handleAuditExport;
164
168
  private handleAuditStats;
@@ -189,6 +193,10 @@ export declare class PayGateServer {
189
193
  * (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
190
194
  * Fire-and-forget: errors logged, never thrown.
191
195
  */
196
+ /**
197
+ * Route admin webhook events through the WebhookRouter (for filter rules) or direct emitter.
198
+ */
199
+ private emitWebhookAdmin;
192
200
  private syncKeyMutation;
193
201
  private readBody;
194
202
  stop(): Promise<void>;
@@ -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
@@ -183,7 +183,7 @@ class PayGateServer {
183
183
  this.audit.log('key.auto_topped_up', 'system', `Auto-topup: added ${amount} credits`, {
184
184
  keyMasked, creditsAdded: amount, newBalance,
185
185
  });
186
- this.gate.webhook?.emitAdmin('key.auto_topped_up', 'system', {
186
+ this.emitWebhookAdmin('key.auto_topped_up', 'system', {
187
187
  keyMasked, creditsAdded: amount, newBalance,
188
188
  });
189
189
  // Sync to Redis (if configured)
@@ -194,7 +194,8 @@ class PayGateServer {
194
194
  // Plugin manager for extensible middleware hooks
195
195
  this.plugins = new plugin_1.PluginManager();
196
196
  this.gate.pluginManager = this.plugins;
197
- this.groups = new groups_1.KeyGroupManager();
197
+ const groupsStatePath = statePath ? statePath.replace(/\.json$/, '-groups.json') : undefined;
198
+ this.groups = new groups_1.KeyGroupManager(groupsStatePath);
198
199
  this.gate.groupManager = this.groups;
199
200
  this.metrics.registerGauge('paygate_plugins_total', 'Number of registered plugins', () => {
200
201
  return this.plugins.count;
@@ -372,6 +373,16 @@ class PayGateServer {
372
373
  break;
373
374
  case '/webhooks/stats':
374
375
  return this.handleWebhookStats(req, res);
376
+ case '/webhooks/filters':
377
+ if (req.method === 'GET')
378
+ return this.handleListWebhookFilters(req, res);
379
+ if (req.method === 'POST')
380
+ return this.handleCreateWebhookFilter(req, res);
381
+ break;
382
+ case '/webhooks/filters/update':
383
+ return this.handleUpdateWebhookFilter(req, res);
384
+ case '/webhooks/filters/delete':
385
+ return this.handleDeleteWebhookFilter(req, res);
375
386
  // ─── Team management endpoints ────────────────────────────────────
376
387
  case '/teams':
377
388
  if (req.method === 'GET')
@@ -610,7 +621,7 @@ class PayGateServer {
610
621
  const fired = this.alerts.check(apiKey, keyRecord, { rateLimitDenied: isRateLimited });
611
622
  // Send alert events via webhook
612
623
  for (const alert of fired) {
613
- this.gate.webhook?.emitAdmin('alert.fired', 'system', {
624
+ this.emitWebhookAdmin('alert.fired', 'system', {
614
625
  alertType: alert.type,
615
626
  keyPrefix: alert.keyPrefix,
616
627
  keyName: alert.keyName,
@@ -824,6 +835,9 @@ class PayGateServer {
824
835
  alerts: 'GET /alerts — Get pending alerts + POST /alerts — Configure alert rules (requires X-Admin-Key)',
825
836
  webhookDeadLetters: 'GET /webhooks/dead-letter — View failed webhook deliveries + DELETE to clear (requires X-Admin-Key)',
826
837
  webhookStats: 'GET /webhooks/stats — Webhook delivery statistics (requires X-Admin-Key)',
838
+ webhookFilters: 'GET|POST /webhooks/filters — List or create webhook filter rules (requires X-Admin-Key)',
839
+ updateWebhookFilter: 'POST /webhooks/filters/update — Update a webhook filter rule (requires X-Admin-Key)',
840
+ deleteWebhookFilter: 'POST /webhooks/filters/delete — Delete a webhook filter rule (requires X-Admin-Key)',
827
841
  teams: 'GET /teams — List teams + POST /teams — Create team (requires X-Admin-Key)',
828
842
  teamsUpdate: 'POST /teams/update — Update team (requires X-Admin-Key)',
829
843
  teamsDelete: 'POST /teams/delete — Delete team (requires X-Admin-Key)',
@@ -963,7 +977,7 @@ class PayGateServer {
963
977
  deniedTools: record.deniedTools,
964
978
  expiresAt: record.expiresAt,
965
979
  });
966
- this.gate.webhook?.emitAdmin('key.created', 'admin', {
980
+ this.emitWebhookAdmin('key.created', 'admin', {
967
981
  keyMasked: (0, audit_1.maskKeyForAudit)(record.key), name, credits,
968
982
  });
969
983
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -1039,7 +1053,7 @@ class PayGateServer {
1039
1053
  creditsAdded: credits,
1040
1054
  newBalance: record?.credits,
1041
1055
  });
1042
- this.gate.webhook?.emitAdmin('key.topup', 'admin', {
1056
+ this.emitWebhookAdmin('key.topup', 'admin', {
1043
1057
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key), creditsAdded: credits, newBalance: record?.credits,
1044
1058
  });
1045
1059
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1085,7 +1099,7 @@ class PayGateServer {
1085
1099
  this.audit.log('key.revoked', 'admin', `Key revoked`, {
1086
1100
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1087
1101
  });
1088
- this.gate.webhook?.emitAdmin('key.revoked', 'admin', {
1102
+ this.emitWebhookAdmin('key.revoked', 'admin', {
1089
1103
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1090
1104
  });
1091
1105
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1131,7 +1145,7 @@ class PayGateServer {
1131
1145
  oldKeyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1132
1146
  newKeyMasked: (0, audit_1.maskKeyForAudit)(rotated.key),
1133
1147
  });
1134
- this.gate.webhook?.emitAdmin('key.rotated', 'admin', {
1148
+ this.emitWebhookAdmin('key.rotated', 'admin', {
1135
1149
  oldKeyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1136
1150
  newKeyMasked: (0, audit_1.maskKeyForAudit)(rotated.key),
1137
1151
  });
@@ -1478,7 +1492,7 @@ class PayGateServer {
1478
1492
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1479
1493
  threshold, amount, maxDaily,
1480
1494
  });
1481
- this.gate.webhook?.emitAdmin('key.auto_topup_configured', 'admin', {
1495
+ this.emitWebhookAdmin('key.auto_topup_configured', 'admin', {
1482
1496
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key), threshold, amount, maxDaily,
1483
1497
  });
1484
1498
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -2083,13 +2097,151 @@ class PayGateServer {
2083
2097
  return;
2084
2098
  }
2085
2099
  const stats = this.gate.webhook.getRetryStats();
2100
+ const routerStats = this.gate.webhookRouter ? this.gate.webhookRouter.getAggregateStats() : null;
2086
2101
  res.writeHead(200, { 'Content-Type': 'application/json' });
2087
2102
  res.end(JSON.stringify({
2088
2103
  configured: true,
2089
2104
  maxRetries: this.gate.webhook.maxRetries,
2090
2105
  ...stats,
2106
+ ...(routerStats ? { filters: routerStats } : {}),
2091
2107
  }));
2092
2108
  }
2109
+ // ─── /webhooks/filters — Webhook filter CRUD ─────────────────────────────
2110
+ handleListWebhookFilters(req, res) {
2111
+ if (!this.checkAdmin(req, res))
2112
+ return;
2113
+ if (!this.gate.webhookRouter) {
2114
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2115
+ res.end(JSON.stringify({ count: 0, filters: [], message: 'No webhook router configured' }));
2116
+ return;
2117
+ }
2118
+ const filters = this.gate.webhookRouter.listRules();
2119
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2120
+ res.end(JSON.stringify({ count: filters.length, filters }));
2121
+ }
2122
+ async handleCreateWebhookFilter(req, res) {
2123
+ if (!this.checkAdmin(req, res, 'admin'))
2124
+ return;
2125
+ if (!this.gate.webhookRouter) {
2126
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2127
+ res.end(JSON.stringify({ error: 'No webhook configured. Set webhookUrl or webhookFilters to enable webhook routing.' }));
2128
+ return;
2129
+ }
2130
+ const body = await this.readBody(req);
2131
+ let params;
2132
+ try {
2133
+ params = JSON.parse(body);
2134
+ }
2135
+ catch {
2136
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2137
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
2138
+ return;
2139
+ }
2140
+ try {
2141
+ const rule = this.gate.webhookRouter.addRule({
2142
+ id: '', // auto-generated
2143
+ name: String(params.name || ''),
2144
+ events: Array.isArray(params.events) ? params.events.map(String) : [],
2145
+ url: String(params.url || ''),
2146
+ secret: params.secret ? String(params.secret) : undefined,
2147
+ keyPrefixes: Array.isArray(params.keyPrefixes) ? params.keyPrefixes.map(String) : undefined,
2148
+ active: params.active !== false,
2149
+ });
2150
+ this.audit.log('webhook_filter.created', 'admin', `Webhook filter created: ${rule.name}`, { filterId: rule.id, name: rule.name });
2151
+ res.writeHead(201, { 'Content-Type': 'application/json' });
2152
+ res.end(JSON.stringify(rule));
2153
+ }
2154
+ catch (err) {
2155
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2156
+ res.end(JSON.stringify({ error: err.message }));
2157
+ }
2158
+ }
2159
+ async handleUpdateWebhookFilter(req, res) {
2160
+ if (req.method !== 'POST') {
2161
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2162
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
2163
+ return;
2164
+ }
2165
+ if (!this.checkAdmin(req, res, 'admin'))
2166
+ return;
2167
+ if (!this.gate.webhookRouter) {
2168
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2169
+ res.end(JSON.stringify({ error: 'No webhook router configured' }));
2170
+ return;
2171
+ }
2172
+ const body = await this.readBody(req);
2173
+ let params;
2174
+ try {
2175
+ params = JSON.parse(body);
2176
+ }
2177
+ catch {
2178
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2179
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
2180
+ return;
2181
+ }
2182
+ const filterId = String(params.id || '');
2183
+ if (!filterId) {
2184
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2185
+ res.end(JSON.stringify({ error: 'Missing id field' }));
2186
+ return;
2187
+ }
2188
+ try {
2189
+ const rule = this.gate.webhookRouter.updateRule(filterId, {
2190
+ name: params.name !== undefined ? String(params.name) : undefined,
2191
+ events: Array.isArray(params.events) ? params.events.map(String) : undefined,
2192
+ url: params.url !== undefined ? String(params.url) : undefined,
2193
+ secret: params.secret !== undefined ? String(params.secret) : undefined,
2194
+ keyPrefixes: Array.isArray(params.keyPrefixes) ? params.keyPrefixes.map(String) : undefined,
2195
+ active: params.active !== undefined ? Boolean(params.active) : undefined,
2196
+ });
2197
+ this.audit.log('webhook_filter.updated', 'admin', `Webhook filter updated: ${rule.name}`, { filterId: rule.id });
2198
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2199
+ res.end(JSON.stringify(rule));
2200
+ }
2201
+ catch (err) {
2202
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2203
+ res.end(JSON.stringify({ error: err.message }));
2204
+ }
2205
+ }
2206
+ async handleDeleteWebhookFilter(req, res) {
2207
+ if (req.method !== 'POST') {
2208
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2209
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
2210
+ return;
2211
+ }
2212
+ if (!this.checkAdmin(req, res, 'admin'))
2213
+ return;
2214
+ if (!this.gate.webhookRouter) {
2215
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2216
+ res.end(JSON.stringify({ error: 'No webhook router configured' }));
2217
+ return;
2218
+ }
2219
+ const body = await this.readBody(req);
2220
+ let params;
2221
+ try {
2222
+ params = JSON.parse(body);
2223
+ }
2224
+ catch {
2225
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2226
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
2227
+ return;
2228
+ }
2229
+ const filterId = String(params.id || '');
2230
+ if (!filterId) {
2231
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2232
+ res.end(JSON.stringify({ error: 'Missing id field' }));
2233
+ return;
2234
+ }
2235
+ const deleted = this.gate.webhookRouter.deleteRule(filterId);
2236
+ if (!deleted) {
2237
+ res.writeHead(404, { 'Content-Type': 'application/json' });
2238
+ res.end(JSON.stringify({ error: 'Filter not found' }));
2239
+ return;
2240
+ }
2241
+ this.audit.log('webhook_filter.deleted', 'admin', `Webhook filter deleted: ${filterId}`, { filterId });
2242
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2243
+ res.end(JSON.stringify({ ok: true, message: `Filter ${filterId} deleted` }));
2244
+ }
2093
2245
  // ─── /audit — Query audit log ─────────────────────────────────────────────
2094
2246
  handleAudit(req, res) {
2095
2247
  if (req.method !== 'GET') {
@@ -2621,6 +2773,7 @@ class PayGateServer {
2621
2773
  tags: params.tags,
2622
2774
  });
2623
2775
  this.audit.log('group.created', 'admin', `Group created: ${group.name}`, { groupId: group.id, name: group.name });
2776
+ this.groups.saveToFile();
2624
2777
  if (this.redisSync)
2625
2778
  this.redisSync.saveGroup(group).catch(() => { });
2626
2779
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -2675,6 +2828,7 @@ class PayGateServer {
2675
2828
  tags: params.tags,
2676
2829
  });
2677
2830
  this.audit.log('group.updated', 'admin', `Group updated: ${group.name}`, { groupId: group.id });
2831
+ this.groups.saveToFile();
2678
2832
  if (this.redisSync)
2679
2833
  this.redisSync.saveGroup(group).catch(() => { });
2680
2834
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -2716,6 +2870,7 @@ class PayGateServer {
2716
2870
  return;
2717
2871
  }
2718
2872
  this.audit.log('group.deleted', 'admin', `Group deleted: ${groupId}`, { groupId });
2873
+ this.groups.saveToFile();
2719
2874
  if (this.redisSync) {
2720
2875
  this.redisSync.deleteGroup(groupId).catch(() => { });
2721
2876
  this.redisSync.saveGroupAssignments().catch(() => { });
@@ -2762,6 +2917,7 @@ class PayGateServer {
2762
2917
  this.audit.log('group.key_assigned', 'admin', `Key assigned to group ${groupId}`, {
2763
2918
  keyMasked: (0, audit_1.maskKeyForAudit)(apiKey), groupId,
2764
2919
  });
2920
+ this.groups.saveToFile();
2765
2921
  if (this.redisSync) {
2766
2922
  this.redisSync.saveGroupAssignments().catch(() => { });
2767
2923
  this.syncKeyMutation(apiKey);
@@ -2810,6 +2966,7 @@ class PayGateServer {
2810
2966
  delete keyRecord.group;
2811
2967
  }
2812
2968
  this.audit.log('group.key_removed', 'admin', `Key removed from group`, { keyMasked: (0, audit_1.maskKeyForAudit)(apiKey) });
2969
+ this.groups.saveToFile();
2813
2970
  if (this.redisSync) {
2814
2971
  this.redisSync.saveGroupAssignments().catch(() => { });
2815
2972
  this.syncKeyMutation(apiKey);
@@ -2875,7 +3032,7 @@ class PayGateServer {
2875
3032
  newKeyMasked: record.key.slice(0, 7) + '...' + record.key.slice(-4),
2876
3033
  role,
2877
3034
  });
2878
- this.gate.webhook?.emitAdmin('admin_key.created', callerMasked, {
3035
+ this.emitWebhookAdmin('admin_key.created', callerMasked, {
2879
3036
  newKeyMasked: record.key.slice(0, 7) + '...' + record.key.slice(-4),
2880
3037
  name: params.name,
2881
3038
  role,
@@ -2929,7 +3086,7 @@ class PayGateServer {
2929
3086
  this.audit.log('admin_key.revoked', callerMasked, `Revoked admin key ${targetMasked}`, {
2930
3087
  revokedKeyMasked: targetMasked,
2931
3088
  });
2932
- this.gate.webhook?.emitAdmin('admin_key.revoked', callerMasked, {
3089
+ this.emitWebhookAdmin('admin_key.revoked', callerMasked, {
2933
3090
  revokedKeyMasked: targetMasked,
2934
3091
  });
2935
3092
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -2940,6 +3097,17 @@ class PayGateServer {
2940
3097
  * (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
2941
3098
  * Fire-and-forget: errors logged, never thrown.
2942
3099
  */
3100
+ /**
3101
+ * Route admin webhook events through the WebhookRouter (for filter rules) or direct emitter.
3102
+ */
3103
+ emitWebhookAdmin(type, actor, metadata = {}) {
3104
+ if (this.gate.webhookRouter) {
3105
+ this.gate.webhookRouter.emitAdmin(type, actor, metadata);
3106
+ }
3107
+ else if (this.gate.webhook) {
3108
+ this.gate.webhook.emitAdmin(type, actor, metadata);
3109
+ }
3110
+ }
2943
3111
  syncKeyMutation(apiKey) {
2944
3112
  if (!this.redisSync)
2945
3113
  return;