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/README.md +84 -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 +4 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +25 -6
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +2 -1
- 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 +171 -9
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/webhook-router.d.ts +89 -0
- package/dist/webhook-router.d.ts.map +1 -0
- package/dist/webhook-router.js +249 -0
- package/dist/webhook-router.js.map +1 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|