paygate-mcp 10.3.0 → 10.4.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/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +382 -0
- package/dist/server.js.map +1 -1
- package/dist/spend-caps.d.ts +105 -0
- package/dist/spend-caps.d.ts.map +1 -0
- package/dist/spend-caps.js +225 -0
- package/dist/spend-caps.js.map +1 -0
- package/dist/task-manager.d.ts +158 -0
- package/dist/task-manager.d.ts.map +1 -0
- package/dist/task-manager.js +302 -0
- package/dist/task-manager.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -127,6 +127,8 @@ const scheduled_reports_1 = require("./scheduled-reports");
|
|
|
127
127
|
const approval_workflows_1 = require("./approval-workflows");
|
|
128
128
|
const gateway_hooks_1 = require("./gateway-hooks");
|
|
129
129
|
const dynamic_discovery_1 = require("./dynamic-discovery");
|
|
130
|
+
const spend_caps_1 = require("./spend-caps");
|
|
131
|
+
const task_manager_1 = require("./task-manager");
|
|
130
132
|
/** Max request body size: 1MB */
|
|
131
133
|
const MAX_BODY_SIZE = 1_048_576;
|
|
132
134
|
/**
|
|
@@ -496,6 +498,10 @@ class PayGateServer {
|
|
|
496
498
|
scheduledReports;
|
|
497
499
|
approvalWorkflows;
|
|
498
500
|
gatewayHooks;
|
|
501
|
+
/** Spend cap manager for server-wide and per-key caps. Null = disabled. */
|
|
502
|
+
spendCaps = null;
|
|
503
|
+
/** MCP Task manager for async task lifecycle. */
|
|
504
|
+
taskManager;
|
|
499
505
|
/** Cached backend tool list for dynamic discovery mode. */
|
|
500
506
|
cachedBackendTools = null;
|
|
501
507
|
/** The active request handler — either proxy or router */
|
|
@@ -524,6 +530,18 @@ class PayGateServer {
|
|
|
524
530
|
this.publicRateLimiter = new rate_limiter_1.RateLimiter(this.config.publicRateLimit ?? 300);
|
|
525
531
|
this.gate = new gate_1.Gate(this.config, statePath);
|
|
526
532
|
this.gate.store.logger = this.logger;
|
|
533
|
+
// Spend caps: server-wide and per-key caps with auto-suspend
|
|
534
|
+
if (this.config.spendCaps) {
|
|
535
|
+
this.spendCaps = new spend_caps_1.SpendCapManager(this.config.spendCaps);
|
|
536
|
+
this.spendCaps.onAutoSuspend = (prefix, reason) => {
|
|
537
|
+
this.logger.warn(`Auto-suspended key ${prefix}: ${reason}`);
|
|
538
|
+
};
|
|
539
|
+
this.spendCaps.onAutoResume = (prefix) => {
|
|
540
|
+
this.logger.info(`Auto-resumed key ${prefix}`);
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
// MCP Tasks lifecycle manager
|
|
544
|
+
this.taskManager = new task_manager_1.TaskManager();
|
|
527
545
|
// Multi-server mode: use Router
|
|
528
546
|
if (servers && servers.length > 0) {
|
|
529
547
|
this.router = new router_1.MultiServerRouter(this.gate, servers);
|
|
@@ -1548,6 +1566,10 @@ class PayGateServer {
|
|
|
1548
1566
|
return this.handleApprovalWorkflows(req, res);
|
|
1549
1567
|
case '/admin/gateway-hooks':
|
|
1550
1568
|
return this.handleGatewayHooks(req, res);
|
|
1569
|
+
case '/admin/spend-caps':
|
|
1570
|
+
return this.handleSpendCaps(req, res);
|
|
1571
|
+
case '/admin/tasks':
|
|
1572
|
+
return this.handleAdminTasks(req, res);
|
|
1551
1573
|
case '/keys/geo':
|
|
1552
1574
|
return this.handleKeyGeo(req, res);
|
|
1553
1575
|
case '/admin/sla':
|
|
@@ -1834,6 +1856,8 @@ class PayGateServer {
|
|
|
1834
1856
|
// ─── OAuth 2.1 endpoints ─────────────────────────────────────────
|
|
1835
1857
|
case '/.well-known/oauth-authorization-server':
|
|
1836
1858
|
return this.handleOAuthMetadata(req, res);
|
|
1859
|
+
case '/.well-known/oauth-protected-resource':
|
|
1860
|
+
return this.handleOAuthProtectedResource(req, res);
|
|
1837
1861
|
case '/oauth/register':
|
|
1838
1862
|
return this.handleOAuthRegister(req, res);
|
|
1839
1863
|
case '/oauth/authorize':
|
|
@@ -2123,6 +2147,189 @@ class PayGateServer {
|
|
|
2123
2147
|
// If not a meta-tool, fall through to normal tools/call handling
|
|
2124
2148
|
}
|
|
2125
2149
|
}
|
|
2150
|
+
// ─── Spend Caps: server-wide and per-key hourly caps ─────────────────
|
|
2151
|
+
if (this.spendCaps && (request.method === 'tools/call' || request.method === 'tasks/send')) {
|
|
2152
|
+
const keyPrefix = apiKey ? apiKey.slice(0, 10) : 'anon';
|
|
2153
|
+
// Check auto-suspend status (cooldown period)
|
|
2154
|
+
if (apiKey && this.spendCaps.isAutoSuspended(keyPrefix)) {
|
|
2155
|
+
const status = this.spendCaps.getAutoSuspendStatus(keyPrefix);
|
|
2156
|
+
const errResp = {
|
|
2157
|
+
jsonrpc: '2.0',
|
|
2158
|
+
id: request.id,
|
|
2159
|
+
error: {
|
|
2160
|
+
code: -32402,
|
|
2161
|
+
message: `Key auto-suspended due to spend cap breach${status.autoResumeIn ? ` (resumes in ${status.autoResumeIn}s)` : ''}`,
|
|
2162
|
+
data: { autoSuspended: true, autoResumeIn: status.autoResumeIn },
|
|
2163
|
+
},
|
|
2164
|
+
};
|
|
2165
|
+
const accept = req.headers['accept'] || '';
|
|
2166
|
+
if (accept.includes('text/event-stream')) {
|
|
2167
|
+
(0, session_1.writeSseHeaders)(res, { 'Mcp-Session-Id': sessionId });
|
|
2168
|
+
(0, session_1.writeSseEvent)(res, errResp, 'message');
|
|
2169
|
+
res.end();
|
|
2170
|
+
}
|
|
2171
|
+
else {
|
|
2172
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId });
|
|
2173
|
+
res.end(JSON.stringify(errResp));
|
|
2174
|
+
}
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
// Check server-wide caps
|
|
2178
|
+
const toolName = request.params?.name || '';
|
|
2179
|
+
const creditsRequired = this.gate.getToolPrice(toolName, request.params?.arguments, apiKey || undefined);
|
|
2180
|
+
const serverCapResult = this.spendCaps.checkServerCap(creditsRequired);
|
|
2181
|
+
if (!serverCapResult.allowed) {
|
|
2182
|
+
const errResp = {
|
|
2183
|
+
jsonrpc: '2.0',
|
|
2184
|
+
id: request.id,
|
|
2185
|
+
error: { code: -32402, message: `Server spend cap: ${serverCapResult.reason}` },
|
|
2186
|
+
};
|
|
2187
|
+
const accept = req.headers['accept'] || '';
|
|
2188
|
+
if (accept.includes('text/event-stream')) {
|
|
2189
|
+
(0, session_1.writeSseHeaders)(res, { 'Mcp-Session-Id': sessionId });
|
|
2190
|
+
(0, session_1.writeSseEvent)(res, errResp, 'message');
|
|
2191
|
+
res.end();
|
|
2192
|
+
}
|
|
2193
|
+
else {
|
|
2194
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId });
|
|
2195
|
+
res.end(JSON.stringify(errResp));
|
|
2196
|
+
}
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
// Check per-key hourly caps
|
|
2200
|
+
if (apiKey) {
|
|
2201
|
+
const keyRecord = this.gate.store.getKey(apiKey);
|
|
2202
|
+
const quota = keyRecord?.quota || this.config.globalQuota;
|
|
2203
|
+
const hourlyResult = this.spendCaps.checkHourlyCap(keyPrefix, creditsRequired, quota);
|
|
2204
|
+
if (!hourlyResult.allowed) {
|
|
2205
|
+
// Auto-suspend if configured
|
|
2206
|
+
if (hourlyResult.shouldSuspend) {
|
|
2207
|
+
this.spendCaps.autoSuspendKey(keyPrefix, hourlyResult.reason);
|
|
2208
|
+
this.audit.log('key.auto_suspended', keyPrefix, `Auto-suspended: ${hourlyResult.reason}`);
|
|
2209
|
+
}
|
|
2210
|
+
const errResp = {
|
|
2211
|
+
jsonrpc: '2.0',
|
|
2212
|
+
id: request.id,
|
|
2213
|
+
error: { code: -32402, message: `Spend cap: ${hourlyResult.reason}` },
|
|
2214
|
+
};
|
|
2215
|
+
const accept = req.headers['accept'] || '';
|
|
2216
|
+
if (accept.includes('text/event-stream')) {
|
|
2217
|
+
(0, session_1.writeSseHeaders)(res, { 'Mcp-Session-Id': sessionId });
|
|
2218
|
+
(0, session_1.writeSseEvent)(res, errResp, 'message');
|
|
2219
|
+
res.end();
|
|
2220
|
+
}
|
|
2221
|
+
else {
|
|
2222
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId });
|
|
2223
|
+
res.end(JSON.stringify(errResp));
|
|
2224
|
+
}
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// ─── MCP Tasks: handle tasks/* methods ──────────────────────────────
|
|
2230
|
+
if (request.method.startsWith('tasks/')) {
|
|
2231
|
+
const taskSessionId = sessionId || 'default';
|
|
2232
|
+
const keyPrefix = apiKey ? apiKey.slice(0, 10) : 'anon';
|
|
2233
|
+
// tasks/send — Create async task (wraps a tool call)
|
|
2234
|
+
if (request.method === 'tasks/send') {
|
|
2235
|
+
const params = request.params || {};
|
|
2236
|
+
const toolName = String(params.name || '');
|
|
2237
|
+
const toolArgs = params.arguments;
|
|
2238
|
+
if (!toolName) {
|
|
2239
|
+
const errResp = {
|
|
2240
|
+
jsonrpc: '2.0',
|
|
2241
|
+
id: request.id,
|
|
2242
|
+
error: { code: -32602, message: 'Invalid params: "name" (tool name) is required for tasks/send' },
|
|
2243
|
+
};
|
|
2244
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId });
|
|
2245
|
+
res.end(JSON.stringify(errResp));
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
// Gate the task creation (pre-charge credits)
|
|
2249
|
+
if (!this.gate.isFreeMethod('tasks/send')) {
|
|
2250
|
+
const decision = this.gate.evaluate(apiKey, { name: toolName, arguments: toolArgs }, clientIp, scopedTokenTools, countryCode);
|
|
2251
|
+
if (!decision.allowed) {
|
|
2252
|
+
const errResp = {
|
|
2253
|
+
jsonrpc: '2.0',
|
|
2254
|
+
id: request.id,
|
|
2255
|
+
error: { code: -32402, message: `Payment required: ${decision.reason}`, data: { creditsRequired: this.gate.getToolPrice(toolName), remainingCredits: decision.remainingCredits } },
|
|
2256
|
+
};
|
|
2257
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId });
|
|
2258
|
+
res.end(JSON.stringify(errResp));
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
// Create task
|
|
2263
|
+
const creditsCharged = this.gate.isFreeMethod('tasks/send') ? 0 : this.gate.getToolPrice(toolName, toolArgs, apiKey || undefined);
|
|
2264
|
+
const task = this.taskManager.createTask({ toolName, arguments: toolArgs, apiKeyPrefix: keyPrefix, sessionId: taskSessionId, creditsCharged });
|
|
2265
|
+
// Record spend cap usage
|
|
2266
|
+
if (this.spendCaps) {
|
|
2267
|
+
this.spendCaps.record(keyPrefix, creditsCharged);
|
|
2268
|
+
}
|
|
2269
|
+
// Start the task asynchronously
|
|
2270
|
+
this.taskManager.startTask(task.id);
|
|
2271
|
+
// Execute the tool call in the background
|
|
2272
|
+
const taskToolRequest = {
|
|
2273
|
+
jsonrpc: '2.0',
|
|
2274
|
+
id: `task_${task.id}`,
|
|
2275
|
+
method: 'tools/call',
|
|
2276
|
+
params: { name: toolName, arguments: toolArgs },
|
|
2277
|
+
};
|
|
2278
|
+
this.handler.handleRequest(taskToolRequest, apiKey, clientIp, scopedTokenTools, countryCode)
|
|
2279
|
+
.then(response => {
|
|
2280
|
+
if (response.error) {
|
|
2281
|
+
this.taskManager.failTask(task.id, response.error.message);
|
|
2282
|
+
// Refund on failure
|
|
2283
|
+
if (this.gate.refundOnFailure && creditsCharged > 0 && apiKey) {
|
|
2284
|
+
this.gate.refund(apiKey, toolName, creditsCharged);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
else {
|
|
2288
|
+
this.taskManager.completeTask(task.id, response.result);
|
|
2289
|
+
}
|
|
2290
|
+
})
|
|
2291
|
+
.catch(err => {
|
|
2292
|
+
this.taskManager.failTask(task.id, err.message);
|
|
2293
|
+
});
|
|
2294
|
+
// Return task ID immediately
|
|
2295
|
+
const taskResponse = {
|
|
2296
|
+
jsonrpc: '2.0',
|
|
2297
|
+
id: request.id,
|
|
2298
|
+
result: { taskId: task.id, status: task.status, toolName, createdAt: task.createdAt },
|
|
2299
|
+
};
|
|
2300
|
+
const accept = req.headers['accept'] || '';
|
|
2301
|
+
if (accept.includes('text/event-stream')) {
|
|
2302
|
+
(0, session_1.writeSseHeaders)(res, { 'Mcp-Session-Id': sessionId });
|
|
2303
|
+
(0, session_1.writeSseEvent)(res, taskResponse, 'message');
|
|
2304
|
+
res.end();
|
|
2305
|
+
}
|
|
2306
|
+
else {
|
|
2307
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId });
|
|
2308
|
+
res.end(JSON.stringify(taskResponse));
|
|
2309
|
+
}
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
// tasks/get, tasks/result, tasks/list, tasks/cancel — handled by TaskManager
|
|
2313
|
+
const taskResult = this.taskManager.handleTasksMethod(request.method, request.params || {}, keyPrefix, taskSessionId);
|
|
2314
|
+
if (taskResult) {
|
|
2315
|
+
const taskResponse = {
|
|
2316
|
+
jsonrpc: '2.0',
|
|
2317
|
+
id: request.id,
|
|
2318
|
+
result: JSON.parse(taskResult.content[0].text),
|
|
2319
|
+
};
|
|
2320
|
+
const accept = req.headers['accept'] || '';
|
|
2321
|
+
if (accept.includes('text/event-stream')) {
|
|
2322
|
+
(0, session_1.writeSseHeaders)(res, { 'Mcp-Session-Id': sessionId });
|
|
2323
|
+
(0, session_1.writeSseEvent)(res, taskResponse, 'message');
|
|
2324
|
+
res.end();
|
|
2325
|
+
}
|
|
2326
|
+
else {
|
|
2327
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId });
|
|
2328
|
+
res.end(JSON.stringify(taskResponse));
|
|
2329
|
+
}
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2126
2333
|
// Plugin: beforeToolCall — let plugins modify the request before forwarding
|
|
2127
2334
|
let pluginRequest = request;
|
|
2128
2335
|
if (this.plugins.count > 0 && request.method === 'tools/call') {
|
|
@@ -2416,6 +2623,10 @@ class PayGateServer {
|
|
|
2416
2623
|
// Estimate credits from config (actual deduction tracked in gate)
|
|
2417
2624
|
const price = this.gate.getToolPrice(toolName, request.params?.arguments);
|
|
2418
2625
|
this.metrics.recordToolCall(toolName, true, price);
|
|
2626
|
+
// Record spend cap usage for successful tool calls
|
|
2627
|
+
if (this.spendCaps && apiKey) {
|
|
2628
|
+
this.spendCaps.record(apiKey.slice(0, 10), price);
|
|
2629
|
+
}
|
|
2419
2630
|
}
|
|
2420
2631
|
}
|
|
2421
2632
|
// Check alert rules after gate evaluation
|
|
@@ -2920,8 +3131,11 @@ class PayGateServer {
|
|
|
2920
3131
|
adminScheduledReports: 'GET /admin/scheduled-reports — View schedules and stats; POST /admin/scheduled-reports — Create/update/delete/generate report schedules (requires X-Admin-Key)',
|
|
2921
3132
|
adminApprovalWorkflows: 'GET /admin/approval-workflows — View rules and requests; POST /admin/approval-workflows — Create/update/delete rules, approve/deny requests (requires X-Admin-Key)',
|
|
2922
3133
|
adminGatewayHooks: 'GET /admin/gateway-hooks — View hooks and stats; POST /admin/gateway-hooks — Register/update/delete hooks, test execution (requires X-Admin-Key)',
|
|
3134
|
+
adminSpendCaps: 'GET /admin/spend-caps — View spend cap stats and auto-suspended keys; POST /admin/spend-caps — Update config, clear auto-suspend (requires X-Admin-Key)',
|
|
3135
|
+
adminTasks: 'GET /admin/tasks — View MCP task lifecycle status and stats (requires X-Admin-Key)',
|
|
2923
3136
|
...(this.oauth ? {
|
|
2924
3137
|
oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
|
|
3138
|
+
oauthProtectedResource: 'GET /.well-known/oauth-protected-resource — Protected Resource Metadata (RFC 9728)',
|
|
2925
3139
|
oauthRegister: 'POST /oauth/register — Register OAuth client',
|
|
2926
3140
|
oauthAuthorize: 'GET /oauth/authorize — Authorization endpoint',
|
|
2927
3141
|
oauthToken: 'POST /oauth/token — Token endpoint',
|
|
@@ -5778,6 +5992,24 @@ class PayGateServer {
|
|
|
5778
5992
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5779
5993
|
res.end(JSON.stringify(this.oauth.getMetadata()));
|
|
5780
5994
|
}
|
|
5995
|
+
/** GET /.well-known/oauth-protected-resource — Protected Resource Metadata (RFC 9728) */
|
|
5996
|
+
handleOAuthProtectedResource(_req, res) {
|
|
5997
|
+
if (!this.oauth) {
|
|
5998
|
+
this.sendError(res, 404, 'OAuth not enabled');
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
6001
|
+
const metadata = this.oauth.getMetadata();
|
|
6002
|
+
const protectedResourceMetadata = {
|
|
6003
|
+
resource: metadata.issuer,
|
|
6004
|
+
authorization_servers: [metadata.issuer],
|
|
6005
|
+
bearer_methods_supported: ['header'],
|
|
6006
|
+
scopes_supported: metadata.scopes_supported,
|
|
6007
|
+
resource_documentation: 'https://paygated.dev',
|
|
6008
|
+
resource_signing_alg_values_supported: ['RS256'],
|
|
6009
|
+
};
|
|
6010
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
6011
|
+
res.end(JSON.stringify(protectedResourceMetadata));
|
|
6012
|
+
}
|
|
5781
6013
|
/** POST /oauth/register — Dynamic Client Registration (RFC 7591) */
|
|
5782
6014
|
async handleOAuthRegister(req, res) {
|
|
5783
6015
|
if (!this.oauth) {
|
|
@@ -6130,6 +6362,7 @@ class PayGateServer {
|
|
|
6130
6362
|
dashboard: '/dashboard',
|
|
6131
6363
|
paymentMetadata: '/.well-known/mcp-payment',
|
|
6132
6364
|
oauthMetadata: this.oauth ? '/.well-known/oauth-authorization-server' : undefined,
|
|
6365
|
+
oauthProtectedResource: this.oauth ? '/.well-known/oauth-protected-resource' : undefined,
|
|
6133
6366
|
},
|
|
6134
6367
|
links: {
|
|
6135
6368
|
homepage: 'https://paygated.dev',
|
|
@@ -12886,6 +13119,7 @@ class PayGateServer {
|
|
|
12886
13119
|
this.adminRateLimiter.destroy();
|
|
12887
13120
|
this.sessionRateLimiter.destroy();
|
|
12888
13121
|
this.publicRateLimiter.destroy();
|
|
13122
|
+
this.taskManager.destroy();
|
|
12889
13123
|
if (this.redisSync) {
|
|
12890
13124
|
await this.redisSync.destroy();
|
|
12891
13125
|
}
|
|
@@ -12962,6 +13196,7 @@ class PayGateServer {
|
|
|
12962
13196
|
this.adminRateLimiter.destroy();
|
|
12963
13197
|
this.sessionRateLimiter.destroy();
|
|
12964
13198
|
this.publicRateLimiter.destroy();
|
|
13199
|
+
this.taskManager.destroy();
|
|
12965
13200
|
if (this.redisSync) {
|
|
12966
13201
|
await this.redisSync.destroy();
|
|
12967
13202
|
}
|
|
@@ -15668,6 +15903,153 @@ class PayGateServer {
|
|
|
15668
15903
|
}
|
|
15669
15904
|
this.sendError(res, 405, 'Method not allowed. Use GET/POST/DELETE.');
|
|
15670
15905
|
}
|
|
15906
|
+
// ─── /admin/spend-caps — Spend Cap Management ────────────────────────────────
|
|
15907
|
+
async handleSpendCaps(req, res) {
|
|
15908
|
+
if (req.method === 'GET') {
|
|
15909
|
+
if (!this.checkAdmin(req, res, 'viewer'))
|
|
15910
|
+
return;
|
|
15911
|
+
if (!this.spendCaps) {
|
|
15912
|
+
this.sendJson(res, 200, { enabled: false, message: 'Spend caps not configured' });
|
|
15913
|
+
return;
|
|
15914
|
+
}
|
|
15915
|
+
const url = new URL(req.url || '/', `http://localhost`);
|
|
15916
|
+
const keyPrefix = url.searchParams.get('keyPrefix');
|
|
15917
|
+
if (keyPrefix) {
|
|
15918
|
+
// Per-key spend cap info
|
|
15919
|
+
const hourlyStats = this.spendCaps.getKeyHourlyStats(keyPrefix);
|
|
15920
|
+
const suspendStatus = this.spendCaps.getAutoSuspendStatus(keyPrefix);
|
|
15921
|
+
this.sendJson(res, 200, { keyPrefix, hourlyStats, suspendStatus });
|
|
15922
|
+
return;
|
|
15923
|
+
}
|
|
15924
|
+
// Server-wide stats
|
|
15925
|
+
const serverStats = this.spendCaps.getServerStats();
|
|
15926
|
+
const config = this.spendCaps.currentConfig;
|
|
15927
|
+
this.sendJson(res, 200, {
|
|
15928
|
+
enabled: true,
|
|
15929
|
+
config,
|
|
15930
|
+
serverStats,
|
|
15931
|
+
suspendedCount: this.spendCaps.suspendedCount,
|
|
15932
|
+
});
|
|
15933
|
+
return;
|
|
15934
|
+
}
|
|
15935
|
+
if (req.method === 'POST') {
|
|
15936
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
15937
|
+
return;
|
|
15938
|
+
if (!this.spendCaps) {
|
|
15939
|
+
this.sendError(res, 400, 'Spend caps not configured. Enable via spendCaps config.');
|
|
15940
|
+
return;
|
|
15941
|
+
}
|
|
15942
|
+
const raw = await this.readBody(req);
|
|
15943
|
+
let params;
|
|
15944
|
+
try {
|
|
15945
|
+
params = safeJsonParse(raw);
|
|
15946
|
+
}
|
|
15947
|
+
catch {
|
|
15948
|
+
this.sendError(res, 400, 'Invalid JSON');
|
|
15949
|
+
return;
|
|
15950
|
+
}
|
|
15951
|
+
const action = String(params.action ?? '');
|
|
15952
|
+
switch (action) {
|
|
15953
|
+
case 'updateConfig': {
|
|
15954
|
+
const newConfig = {};
|
|
15955
|
+
if (typeof params.breachAction === 'string' && (params.breachAction === 'deny' || params.breachAction === 'suspend')) {
|
|
15956
|
+
newConfig.breachAction = params.breachAction;
|
|
15957
|
+
}
|
|
15958
|
+
if (typeof params.serverDailyCreditCap === 'number')
|
|
15959
|
+
newConfig.serverDailyCreditCap = params.serverDailyCreditCap;
|
|
15960
|
+
if (typeof params.serverDailyCallCap === 'number')
|
|
15961
|
+
newConfig.serverDailyCallCap = params.serverDailyCallCap;
|
|
15962
|
+
if (typeof params.autoResumeAfterSeconds === 'number')
|
|
15963
|
+
newConfig.autoResumeAfterSeconds = params.autoResumeAfterSeconds;
|
|
15964
|
+
const currentConfig = this.spendCaps.currentConfig;
|
|
15965
|
+
this.spendCaps.updateConfig({ ...currentConfig, ...newConfig });
|
|
15966
|
+
this.sendJson(res, 200, { updated: true, config: this.spendCaps.currentConfig });
|
|
15967
|
+
return;
|
|
15968
|
+
}
|
|
15969
|
+
case 'clearSuspend': {
|
|
15970
|
+
const keyPrefix = String(params.keyPrefix ?? '');
|
|
15971
|
+
if (!keyPrefix) {
|
|
15972
|
+
this.sendError(res, 400, 'keyPrefix is required');
|
|
15973
|
+
return;
|
|
15974
|
+
}
|
|
15975
|
+
const cleared = this.spendCaps.clearAutoSuspend(keyPrefix);
|
|
15976
|
+
this.sendJson(res, 200, { keyPrefix, cleared });
|
|
15977
|
+
return;
|
|
15978
|
+
}
|
|
15979
|
+
default:
|
|
15980
|
+
this.sendError(res, 400, 'Unknown action. Use updateConfig/clearSuspend.');
|
|
15981
|
+
return;
|
|
15982
|
+
}
|
|
15983
|
+
}
|
|
15984
|
+
this.sendError(res, 405, 'Method not allowed. Use GET/POST.');
|
|
15985
|
+
}
|
|
15986
|
+
// ─── /admin/tasks — MCP Task Administration ──────────────────────────────────
|
|
15987
|
+
async handleAdminTasks(req, res) {
|
|
15988
|
+
if (req.method === 'GET') {
|
|
15989
|
+
if (!this.checkAdmin(req, res, 'viewer'))
|
|
15990
|
+
return;
|
|
15991
|
+
const url = new URL(req.url || '/', `http://localhost`);
|
|
15992
|
+
const taskId = url.searchParams.get('taskId');
|
|
15993
|
+
if (taskId) {
|
|
15994
|
+
const task = this.taskManager.getTask(taskId);
|
|
15995
|
+
if (!task) {
|
|
15996
|
+
this.sendError(res, 404, `Task not found: ${taskId}`);
|
|
15997
|
+
return;
|
|
15998
|
+
}
|
|
15999
|
+
this.sendJson(res, 200, task);
|
|
16000
|
+
return;
|
|
16001
|
+
}
|
|
16002
|
+
// List tasks with optional filters
|
|
16003
|
+
const status = url.searchParams.get('status');
|
|
16004
|
+
const sessionId = url.searchParams.get('sessionId') || undefined;
|
|
16005
|
+
const apiKeyPrefix = url.searchParams.get('apiKeyPrefix') || undefined;
|
|
16006
|
+
const pageSize = parseInt(url.searchParams.get('pageSize') || '50', 10);
|
|
16007
|
+
const cursor = url.searchParams.get('cursor') || undefined;
|
|
16008
|
+
const result = this.taskManager.listTasks({ status, sessionId, apiKeyPrefix, pageSize, cursor });
|
|
16009
|
+
const stats = this.taskManager.getStats();
|
|
16010
|
+
this.sendJson(res, 200, { ...result, stats });
|
|
16011
|
+
return;
|
|
16012
|
+
}
|
|
16013
|
+
if (req.method === 'POST') {
|
|
16014
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
16015
|
+
return;
|
|
16016
|
+
const raw = await this.readBody(req);
|
|
16017
|
+
let params;
|
|
16018
|
+
try {
|
|
16019
|
+
params = safeJsonParse(raw);
|
|
16020
|
+
}
|
|
16021
|
+
catch {
|
|
16022
|
+
this.sendError(res, 400, 'Invalid JSON');
|
|
16023
|
+
return;
|
|
16024
|
+
}
|
|
16025
|
+
const action = String(params.action ?? '');
|
|
16026
|
+
switch (action) {
|
|
16027
|
+
case 'cancel': {
|
|
16028
|
+
const taskId = String(params.taskId ?? '');
|
|
16029
|
+
if (!taskId) {
|
|
16030
|
+
this.sendError(res, 400, 'taskId is required');
|
|
16031
|
+
return;
|
|
16032
|
+
}
|
|
16033
|
+
const task = this.taskManager.cancelTask(taskId);
|
|
16034
|
+
if (!task) {
|
|
16035
|
+
this.sendError(res, 404, 'Task not found or cannot be cancelled');
|
|
16036
|
+
return;
|
|
16037
|
+
}
|
|
16038
|
+
this.sendJson(res, 200, task);
|
|
16039
|
+
return;
|
|
16040
|
+
}
|
|
16041
|
+
case 'stats': {
|
|
16042
|
+
const stats = this.taskManager.getStats();
|
|
16043
|
+
this.sendJson(res, 200, { stats, totalTasks: this.taskManager.taskCount });
|
|
16044
|
+
return;
|
|
16045
|
+
}
|
|
16046
|
+
default:
|
|
16047
|
+
this.sendError(res, 400, 'Unknown action. Use cancel/stats.');
|
|
16048
|
+
return;
|
|
16049
|
+
}
|
|
16050
|
+
}
|
|
16051
|
+
this.sendError(res, 405, 'Method not allowed. Use GET/POST.');
|
|
16052
|
+
}
|
|
15671
16053
|
}
|
|
15672
16054
|
exports.PayGateServer = PayGateServer;
|
|
15673
16055
|
//# sourceMappingURL=server.js.map
|