paygate-mcp 3.7.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.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)
@@ -373,6 +373,16 @@ class PayGateServer {
373
373
  break;
374
374
  case '/webhooks/stats':
375
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);
376
386
  // ─── Team management endpoints ────────────────────────────────────
377
387
  case '/teams':
378
388
  if (req.method === 'GET')
@@ -611,7 +621,7 @@ class PayGateServer {
611
621
  const fired = this.alerts.check(apiKey, keyRecord, { rateLimitDenied: isRateLimited });
612
622
  // Send alert events via webhook
613
623
  for (const alert of fired) {
614
- this.gate.webhook?.emitAdmin('alert.fired', 'system', {
624
+ this.emitWebhookAdmin('alert.fired', 'system', {
615
625
  alertType: alert.type,
616
626
  keyPrefix: alert.keyPrefix,
617
627
  keyName: alert.keyName,
@@ -825,6 +835,9 @@ class PayGateServer {
825
835
  alerts: 'GET /alerts — Get pending alerts + POST /alerts — Configure alert rules (requires X-Admin-Key)',
826
836
  webhookDeadLetters: 'GET /webhooks/dead-letter — View failed webhook deliveries + DELETE to clear (requires X-Admin-Key)',
827
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)',
828
841
  teams: 'GET /teams — List teams + POST /teams — Create team (requires X-Admin-Key)',
829
842
  teamsUpdate: 'POST /teams/update — Update team (requires X-Admin-Key)',
830
843
  teamsDelete: 'POST /teams/delete — Delete team (requires X-Admin-Key)',
@@ -964,7 +977,7 @@ class PayGateServer {
964
977
  deniedTools: record.deniedTools,
965
978
  expiresAt: record.expiresAt,
966
979
  });
967
- this.gate.webhook?.emitAdmin('key.created', 'admin', {
980
+ this.emitWebhookAdmin('key.created', 'admin', {
968
981
  keyMasked: (0, audit_1.maskKeyForAudit)(record.key), name, credits,
969
982
  });
970
983
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -1040,7 +1053,7 @@ class PayGateServer {
1040
1053
  creditsAdded: credits,
1041
1054
  newBalance: record?.credits,
1042
1055
  });
1043
- this.gate.webhook?.emitAdmin('key.topup', 'admin', {
1056
+ this.emitWebhookAdmin('key.topup', 'admin', {
1044
1057
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key), creditsAdded: credits, newBalance: record?.credits,
1045
1058
  });
1046
1059
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1086,7 +1099,7 @@ class PayGateServer {
1086
1099
  this.audit.log('key.revoked', 'admin', `Key revoked`, {
1087
1100
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1088
1101
  });
1089
- this.gate.webhook?.emitAdmin('key.revoked', 'admin', {
1102
+ this.emitWebhookAdmin('key.revoked', 'admin', {
1090
1103
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1091
1104
  });
1092
1105
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1132,7 +1145,7 @@ class PayGateServer {
1132
1145
  oldKeyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1133
1146
  newKeyMasked: (0, audit_1.maskKeyForAudit)(rotated.key),
1134
1147
  });
1135
- this.gate.webhook?.emitAdmin('key.rotated', 'admin', {
1148
+ this.emitWebhookAdmin('key.rotated', 'admin', {
1136
1149
  oldKeyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1137
1150
  newKeyMasked: (0, audit_1.maskKeyForAudit)(rotated.key),
1138
1151
  });
@@ -1479,7 +1492,7 @@ class PayGateServer {
1479
1492
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1480
1493
  threshold, amount, maxDaily,
1481
1494
  });
1482
- this.gate.webhook?.emitAdmin('key.auto_topup_configured', 'admin', {
1495
+ this.emitWebhookAdmin('key.auto_topup_configured', 'admin', {
1483
1496
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key), threshold, amount, maxDaily,
1484
1497
  });
1485
1498
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -2084,13 +2097,151 @@ class PayGateServer {
2084
2097
  return;
2085
2098
  }
2086
2099
  const stats = this.gate.webhook.getRetryStats();
2100
+ const routerStats = this.gate.webhookRouter ? this.gate.webhookRouter.getAggregateStats() : null;
2087
2101
  res.writeHead(200, { 'Content-Type': 'application/json' });
2088
2102
  res.end(JSON.stringify({
2089
2103
  configured: true,
2090
2104
  maxRetries: this.gate.webhook.maxRetries,
2091
2105
  ...stats,
2106
+ ...(routerStats ? { filters: routerStats } : {}),
2092
2107
  }));
2093
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
+ }
2094
2245
  // ─── /audit — Query audit log ─────────────────────────────────────────────
2095
2246
  handleAudit(req, res) {
2096
2247
  if (req.method !== 'GET') {
@@ -2881,7 +3032,7 @@ class PayGateServer {
2881
3032
  newKeyMasked: record.key.slice(0, 7) + '...' + record.key.slice(-4),
2882
3033
  role,
2883
3034
  });
2884
- this.gate.webhook?.emitAdmin('admin_key.created', callerMasked, {
3035
+ this.emitWebhookAdmin('admin_key.created', callerMasked, {
2885
3036
  newKeyMasked: record.key.slice(0, 7) + '...' + record.key.slice(-4),
2886
3037
  name: params.name,
2887
3038
  role,
@@ -2935,7 +3086,7 @@ class PayGateServer {
2935
3086
  this.audit.log('admin_key.revoked', callerMasked, `Revoked admin key ${targetMasked}`, {
2936
3087
  revokedKeyMasked: targetMasked,
2937
3088
  });
2938
- this.gate.webhook?.emitAdmin('admin_key.revoked', callerMasked, {
3089
+ this.emitWebhookAdmin('admin_key.revoked', callerMasked, {
2939
3090
  revokedKeyMasked: targetMasked,
2940
3091
  });
2941
3092
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -2946,6 +3097,17 @@ class PayGateServer {
2946
3097
  * (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
2947
3098
  * Fire-and-forget: errors logged, never thrown.
2948
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
+ }
2949
3111
  syncKeyMutation(apiKey) {
2950
3112
  if (!this.redisSync)
2951
3113
  return;