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/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