paygate-mcp 7.2.0 → 7.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/README.md +109 -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/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +379 -0
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -231,6 +231,16 @@ class PayGateServer {
|
|
|
231
231
|
scheduleTimer = null;
|
|
232
232
|
/** Auto-incrementing schedule ID counter */
|
|
233
233
|
nextScheduleId = 1;
|
|
234
|
+
/** Credit reservations (holds) */
|
|
235
|
+
creditReservations = new Map();
|
|
236
|
+
/** Auto-incrementing reservation ID counter */
|
|
237
|
+
nextReservationId = 1;
|
|
238
|
+
/** Request log — ring buffer of tool call entries */
|
|
239
|
+
requestLog = [];
|
|
240
|
+
/** Next request log entry ID */
|
|
241
|
+
nextRequestLogId = 1;
|
|
242
|
+
/** Max request log entries (ring buffer) */
|
|
243
|
+
maxRequestLogEntries = 5000;
|
|
234
244
|
/** Number of in-flight /mcp requests */
|
|
235
245
|
inflight = 0;
|
|
236
246
|
/** Config file path for hot reload (null if not using config file) */
|
|
@@ -574,6 +584,26 @@ class PayGateServer {
|
|
|
574
584
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
575
585
|
res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
|
|
576
586
|
return;
|
|
587
|
+
case '/keys/reserve':
|
|
588
|
+
if (req.method === 'GET')
|
|
589
|
+
return this.handleListReservations(req, res);
|
|
590
|
+
if (req.method === 'POST')
|
|
591
|
+
return this.handleCreateReservation(req, res);
|
|
592
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
593
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
594
|
+
return;
|
|
595
|
+
case '/keys/reserve/commit':
|
|
596
|
+
if (req.method === 'POST')
|
|
597
|
+
return this.handleCommitReservation(req, res);
|
|
598
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
599
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
|
600
|
+
return;
|
|
601
|
+
case '/keys/reserve/release':
|
|
602
|
+
if (req.method === 'POST')
|
|
603
|
+
return this.handleReleaseReservation(req, res);
|
|
604
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
605
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
|
606
|
+
return;
|
|
577
607
|
case '/keys/rotate':
|
|
578
608
|
return this.handleRotateKey(req, res);
|
|
579
609
|
case '/keys/acl':
|
|
@@ -642,6 +672,12 @@ class PayGateServer {
|
|
|
642
672
|
return this.handleAuditExport(req, res);
|
|
643
673
|
case '/audit/stats':
|
|
644
674
|
return this.handleAuditStats(req, res);
|
|
675
|
+
case '/requests':
|
|
676
|
+
if (req.method === 'GET')
|
|
677
|
+
return this.handleRequestLog(req, res);
|
|
678
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
679
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
|
|
680
|
+
return;
|
|
645
681
|
// ─── Registry / Discovery endpoints ──────────────────────────────
|
|
646
682
|
case '/.well-known/mcp-payment':
|
|
647
683
|
return this.handlePaymentMetadata(req, res);
|
|
@@ -891,6 +927,7 @@ class PayGateServer {
|
|
|
891
927
|
const pluginCtx = { apiKey, toolName, toolArgs, request };
|
|
892
928
|
pluginRequest = await this.plugins.executeBeforeToolCall(pluginCtx);
|
|
893
929
|
}
|
|
930
|
+
const toolCallStartTime = Date.now();
|
|
894
931
|
let response = await this.handler.handleRequest(pluginRequest, apiKey, clientIp, scopedTokenTools);
|
|
895
932
|
// Plugin: afterToolCall — let plugins modify the response
|
|
896
933
|
if (this.plugins.count > 0 && request.method === 'tools/call') {
|
|
@@ -958,6 +995,49 @@ class PayGateServer {
|
|
|
958
995
|
}
|
|
959
996
|
}
|
|
960
997
|
}
|
|
998
|
+
// Record request log entry for tools/call
|
|
999
|
+
if (request.method === 'tools/call') {
|
|
1000
|
+
const toolName = request.params?.name || 'unknown';
|
|
1001
|
+
const durationMs = Date.now() - toolCallStartTime;
|
|
1002
|
+
const isError = !!response.error;
|
|
1003
|
+
let denyReason;
|
|
1004
|
+
if (isError) {
|
|
1005
|
+
const msg = response.error.message || '';
|
|
1006
|
+
if (msg.includes('rate_limited'))
|
|
1007
|
+
denyReason = 'rate_limited';
|
|
1008
|
+
else if (msg.includes('insufficient_credits'))
|
|
1009
|
+
denyReason = 'insufficient_credits';
|
|
1010
|
+
else if (msg.includes('invalid_api_key'))
|
|
1011
|
+
denyReason = 'invalid_api_key';
|
|
1012
|
+
else if (msg.includes('key_suspended'))
|
|
1013
|
+
denyReason = 'key_suspended';
|
|
1014
|
+
else if (msg.includes('api_key_expired'))
|
|
1015
|
+
denyReason = 'api_key_expired';
|
|
1016
|
+
else if (msg.includes('tool_not_allowed'))
|
|
1017
|
+
denyReason = 'tool_not_allowed';
|
|
1018
|
+
else if (msg.includes('quota_exceeded'))
|
|
1019
|
+
denyReason = 'quota_exceeded';
|
|
1020
|
+
else
|
|
1021
|
+
denyReason = msg || 'denied';
|
|
1022
|
+
}
|
|
1023
|
+
const creditsCharged = isError ? 0 : this.gate.getToolPrice(toolName, request.params?.arguments);
|
|
1024
|
+
const logEntry = {
|
|
1025
|
+
id: this.nextRequestLogId++,
|
|
1026
|
+
timestamp: new Date().toISOString(),
|
|
1027
|
+
tool: toolName,
|
|
1028
|
+
key: (0, audit_1.maskKeyForAudit)(apiKey || 'anonymous'),
|
|
1029
|
+
status: (isError ? 'denied' : 'allowed'),
|
|
1030
|
+
credits: creditsCharged,
|
|
1031
|
+
durationMs,
|
|
1032
|
+
...(denyReason ? { denyReason } : {}),
|
|
1033
|
+
requestId,
|
|
1034
|
+
};
|
|
1035
|
+
this.requestLog.push(logEntry);
|
|
1036
|
+
// Enforce ring buffer size
|
|
1037
|
+
if (this.requestLog.length > this.maxRequestLogEntries) {
|
|
1038
|
+
this.requestLog = this.requestLog.slice(-this.maxRequestLogEntries);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
961
1041
|
// Build rate limit + credits headers for tools/call responses
|
|
962
1042
|
const rateLimitHeaders = this.buildRateLimitHeaders(apiKey, request);
|
|
963
1043
|
// Check if client accepts SSE
|
|
@@ -1214,6 +1294,8 @@ class PayGateServer {
|
|
|
1214
1294
|
keyNotes: 'GET /keys/notes?key=... — List notes + POST to add + DELETE to remove (requires X-Admin-Key)',
|
|
1215
1295
|
keySchedule: 'GET /keys/schedule?key=... — List schedules + POST to create + DELETE to cancel (requires X-Admin-Key)',
|
|
1216
1296
|
keyActivity: 'GET /keys/activity?key=... — Unified activity timeline for a key (requires X-Admin-Key)',
|
|
1297
|
+
creditReservations: 'POST /keys/reserve to hold credits, POST /keys/reserve/commit to deduct, POST /keys/reserve/release to release, GET /keys/reserve to list (requires X-Admin-Key)',
|
|
1298
|
+
requestLog: 'GET /requests — Queryable log of tool call requests with timing, credits, status (requires X-Admin-Key)',
|
|
1217
1299
|
...(this.oauth ? {
|
|
1218
1300
|
oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
|
|
1219
1301
|
oauthRegister: 'POST /oauth/register — Register OAuth client',
|
|
@@ -4382,6 +4464,244 @@ class PayGateServer {
|
|
|
4382
4464
|
events: page,
|
|
4383
4465
|
}));
|
|
4384
4466
|
}
|
|
4467
|
+
// ─── /keys/reserve — Credit reservations (hold, commit, release) ─────────
|
|
4468
|
+
handleListReservations(req, res) {
|
|
4469
|
+
if (!this.checkAdmin(req, res))
|
|
4470
|
+
return;
|
|
4471
|
+
const urlParts = req.url?.split('?') || [];
|
|
4472
|
+
const params = new URLSearchParams(urlParts[1] || '');
|
|
4473
|
+
const keyParam = params.get('key');
|
|
4474
|
+
// Clean up expired reservations first
|
|
4475
|
+
this.cleanupExpiredReservations();
|
|
4476
|
+
let reservations = [...this.creditReservations.values()];
|
|
4477
|
+
if (keyParam) {
|
|
4478
|
+
const record = this.gate.store.resolveKeyRaw(keyParam);
|
|
4479
|
+
if (!record) {
|
|
4480
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
4481
|
+
res.end(JSON.stringify({ error: 'Key not found' }));
|
|
4482
|
+
return;
|
|
4483
|
+
}
|
|
4484
|
+
reservations = reservations.filter(r => r.key === record.key);
|
|
4485
|
+
}
|
|
4486
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4487
|
+
res.end(JSON.stringify({
|
|
4488
|
+
reservations: reservations.map(r => ({
|
|
4489
|
+
...r,
|
|
4490
|
+
key: (0, audit_1.maskKeyForAudit)(r.key),
|
|
4491
|
+
})),
|
|
4492
|
+
count: reservations.length,
|
|
4493
|
+
totalHeld: reservations.reduce((sum, r) => sum + r.credits, 0),
|
|
4494
|
+
}));
|
|
4495
|
+
}
|
|
4496
|
+
handleCreateReservation(req, res) {
|
|
4497
|
+
if (!this.checkAdmin(req, res))
|
|
4498
|
+
return;
|
|
4499
|
+
let body = '';
|
|
4500
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
4501
|
+
req.on('end', () => {
|
|
4502
|
+
let params;
|
|
4503
|
+
try {
|
|
4504
|
+
params = JSON.parse(body);
|
|
4505
|
+
}
|
|
4506
|
+
catch {
|
|
4507
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4508
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
if (!params.key || typeof params.key !== 'string') {
|
|
4512
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4513
|
+
res.end(JSON.stringify({ error: 'Missing required field: key' }));
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
if (!params.credits || typeof params.credits !== 'number' || params.credits <= 0) {
|
|
4517
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4518
|
+
res.end(JSON.stringify({ error: 'Missing or invalid credits (must be positive number)' }));
|
|
4519
|
+
return;
|
|
4520
|
+
}
|
|
4521
|
+
const record = this.gate.store.resolveKeyRaw(params.key);
|
|
4522
|
+
if (!record) {
|
|
4523
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
4524
|
+
res.end(JSON.stringify({ error: 'Key not found' }));
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
if (!record.active) {
|
|
4528
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4529
|
+
res.end(JSON.stringify({ error: 'Key is revoked' }));
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
if (record.suspended) {
|
|
4533
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4534
|
+
res.end(JSON.stringify({ error: 'Key is suspended' }));
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
// Cleanup expired before checking availability
|
|
4538
|
+
this.cleanupExpiredReservations();
|
|
4539
|
+
// Calculate available credits (total - held)
|
|
4540
|
+
const heldCredits = this.getHeldCredits(record.key);
|
|
4541
|
+
const available = record.credits - heldCredits;
|
|
4542
|
+
if (params.credits > available) {
|
|
4543
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4544
|
+
res.end(JSON.stringify({
|
|
4545
|
+
error: 'Insufficient available credits',
|
|
4546
|
+
available,
|
|
4547
|
+
held: heldCredits,
|
|
4548
|
+
total: record.credits,
|
|
4549
|
+
requested: params.credits,
|
|
4550
|
+
}));
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
// Max 50 active reservations per key
|
|
4554
|
+
const keyReservations = [...this.creditReservations.values()].filter(r => r.key === record.key);
|
|
4555
|
+
if (keyReservations.length >= 50) {
|
|
4556
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4557
|
+
res.end(JSON.stringify({ error: 'Maximum 50 active reservations per key' }));
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
const ttl = Math.min(3600, Math.max(10, params.ttlSeconds || 300)); // 10s – 1h, default 5m
|
|
4561
|
+
const reservation = {
|
|
4562
|
+
id: `rsv_${this.nextReservationId++}`,
|
|
4563
|
+
key: record.key,
|
|
4564
|
+
credits: params.credits,
|
|
4565
|
+
createdAt: new Date().toISOString(),
|
|
4566
|
+
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
|
|
4567
|
+
memo: params.memo?.slice(0, 200),
|
|
4568
|
+
};
|
|
4569
|
+
this.creditReservations.set(reservation.id, reservation);
|
|
4570
|
+
this.audit.log('credits.reserved', 'admin', `Reserved ${params.credits} credits`, {
|
|
4571
|
+
reservationId: reservation.id,
|
|
4572
|
+
key: (0, audit_1.maskKeyForAudit)(record.key),
|
|
4573
|
+
credits: params.credits,
|
|
4574
|
+
ttlSeconds: ttl,
|
|
4575
|
+
});
|
|
4576
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
4577
|
+
res.end(JSON.stringify({
|
|
4578
|
+
...reservation,
|
|
4579
|
+
key: (0, audit_1.maskKeyForAudit)(reservation.key),
|
|
4580
|
+
available: available - params.credits,
|
|
4581
|
+
}));
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
handleCommitReservation(req, res) {
|
|
4585
|
+
if (!this.checkAdmin(req, res))
|
|
4586
|
+
return;
|
|
4587
|
+
let body = '';
|
|
4588
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
4589
|
+
req.on('end', () => {
|
|
4590
|
+
let params;
|
|
4591
|
+
try {
|
|
4592
|
+
params = JSON.parse(body);
|
|
4593
|
+
}
|
|
4594
|
+
catch {
|
|
4595
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4596
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4599
|
+
if (!params.reservationId || typeof params.reservationId !== 'string') {
|
|
4600
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4601
|
+
res.end(JSON.stringify({ error: 'Missing required field: reservationId' }));
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
const reservation = this.creditReservations.get(params.reservationId);
|
|
4605
|
+
if (!reservation) {
|
|
4606
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
4607
|
+
res.end(JSON.stringify({ error: 'Reservation not found (may have expired)' }));
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4610
|
+
// Check if expired
|
|
4611
|
+
if (new Date(reservation.expiresAt).getTime() <= Date.now()) {
|
|
4612
|
+
this.creditReservations.delete(params.reservationId);
|
|
4613
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4614
|
+
res.end(JSON.stringify({ error: 'Reservation has expired' }));
|
|
4615
|
+
return;
|
|
4616
|
+
}
|
|
4617
|
+
const record = this.gate.store.resolveKeyRaw(reservation.key);
|
|
4618
|
+
if (!record) {
|
|
4619
|
+
this.creditReservations.delete(params.reservationId);
|
|
4620
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
4621
|
+
res.end(JSON.stringify({ error: 'Key not found' }));
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4624
|
+
// Deduct credits
|
|
4625
|
+
record.credits = Math.max(0, record.credits - reservation.credits);
|
|
4626
|
+
this.gate.store.save();
|
|
4627
|
+
// Remove reservation
|
|
4628
|
+
this.creditReservations.delete(params.reservationId);
|
|
4629
|
+
this.audit.log('credits.committed', 'admin', `Committed reservation: ${reservation.credits} credits deducted`, {
|
|
4630
|
+
reservationId: reservation.id,
|
|
4631
|
+
key: (0, audit_1.maskKeyForAudit)(record.key),
|
|
4632
|
+
credits: reservation.credits,
|
|
4633
|
+
remainingCredits: record.credits,
|
|
4634
|
+
});
|
|
4635
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4636
|
+
res.end(JSON.stringify({
|
|
4637
|
+
committed: {
|
|
4638
|
+
...reservation,
|
|
4639
|
+
key: (0, audit_1.maskKeyForAudit)(reservation.key),
|
|
4640
|
+
},
|
|
4641
|
+
remainingCredits: record.credits,
|
|
4642
|
+
}));
|
|
4643
|
+
});
|
|
4644
|
+
}
|
|
4645
|
+
handleReleaseReservation(req, res) {
|
|
4646
|
+
if (!this.checkAdmin(req, res))
|
|
4647
|
+
return;
|
|
4648
|
+
let body = '';
|
|
4649
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
4650
|
+
req.on('end', () => {
|
|
4651
|
+
let params;
|
|
4652
|
+
try {
|
|
4653
|
+
params = JSON.parse(body);
|
|
4654
|
+
}
|
|
4655
|
+
catch {
|
|
4656
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4657
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
4658
|
+
return;
|
|
4659
|
+
}
|
|
4660
|
+
if (!params.reservationId || typeof params.reservationId !== 'string') {
|
|
4661
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
4662
|
+
res.end(JSON.stringify({ error: 'Missing required field: reservationId' }));
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
const reservation = this.creditReservations.get(params.reservationId);
|
|
4666
|
+
if (!reservation) {
|
|
4667
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
4668
|
+
res.end(JSON.stringify({ error: 'Reservation not found (may have expired)' }));
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4671
|
+
// Remove reservation (release the hold)
|
|
4672
|
+
this.creditReservations.delete(params.reservationId);
|
|
4673
|
+
this.audit.log('credits.released', 'admin', `Released reservation: ${reservation.credits} credits freed`, {
|
|
4674
|
+
reservationId: reservation.id,
|
|
4675
|
+
key: (0, audit_1.maskKeyForAudit)(reservation.key),
|
|
4676
|
+
credits: reservation.credits,
|
|
4677
|
+
});
|
|
4678
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4679
|
+
res.end(JSON.stringify({
|
|
4680
|
+
released: {
|
|
4681
|
+
...reservation,
|
|
4682
|
+
key: (0, audit_1.maskKeyForAudit)(reservation.key),
|
|
4683
|
+
},
|
|
4684
|
+
}));
|
|
4685
|
+
});
|
|
4686
|
+
}
|
|
4687
|
+
/** Get total held credits for a key across all active reservations. */
|
|
4688
|
+
getHeldCredits(key) {
|
|
4689
|
+
let held = 0;
|
|
4690
|
+
for (const r of this.creditReservations.values()) {
|
|
4691
|
+
if (r.key === key)
|
|
4692
|
+
held += r.credits;
|
|
4693
|
+
}
|
|
4694
|
+
return held;
|
|
4695
|
+
}
|
|
4696
|
+
/** Remove expired reservations. */
|
|
4697
|
+
cleanupExpiredReservations() {
|
|
4698
|
+
const now = Date.now();
|
|
4699
|
+
for (const [id, r] of this.creditReservations) {
|
|
4700
|
+
if (new Date(r.expiresAt).getTime() <= now) {
|
|
4701
|
+
this.creditReservations.delete(id);
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4385
4705
|
// ─── /config/reload — Hot reload configuration from file ─────────────────
|
|
4386
4706
|
async handleConfigReload(req, res) {
|
|
4387
4707
|
if (req.method !== 'POST') {
|
|
@@ -5961,6 +6281,65 @@ class PayGateServer {
|
|
|
5961
6281
|
await this.redisSync.destroy();
|
|
5962
6282
|
}
|
|
5963
6283
|
}
|
|
6284
|
+
// ─── /requests — Request Log (queryable tool call log) ──────────────────────
|
|
6285
|
+
handleRequestLog(req, res) {
|
|
6286
|
+
if (!this.checkAdmin(req, res))
|
|
6287
|
+
return;
|
|
6288
|
+
const urlParts = req.url?.split('?') || [];
|
|
6289
|
+
const params = new URLSearchParams(urlParts[1] || '');
|
|
6290
|
+
// Filters
|
|
6291
|
+
const keyFilter = params.get('key');
|
|
6292
|
+
const toolFilter = params.get('tool');
|
|
6293
|
+
const statusFilter = params.get('status'); // 'allowed' | 'denied'
|
|
6294
|
+
const sinceFilter = params.get('since');
|
|
6295
|
+
const limit = Math.min(1000, Math.max(1, parseInt(params.get('limit') || '100', 10) || 100));
|
|
6296
|
+
const offset = Math.max(0, parseInt(params.get('offset') || '0', 10) || 0);
|
|
6297
|
+
let filtered = this.requestLog;
|
|
6298
|
+
// Filter by key (partial match on masked key)
|
|
6299
|
+
if (keyFilter) {
|
|
6300
|
+
const kf = keyFilter.toLowerCase();
|
|
6301
|
+
filtered = filtered.filter(e => e.key.toLowerCase().includes(kf));
|
|
6302
|
+
}
|
|
6303
|
+
// Filter by tool name (exact match)
|
|
6304
|
+
if (toolFilter) {
|
|
6305
|
+
filtered = filtered.filter(e => e.tool === toolFilter);
|
|
6306
|
+
}
|
|
6307
|
+
// Filter by status
|
|
6308
|
+
if (statusFilter === 'allowed' || statusFilter === 'denied') {
|
|
6309
|
+
filtered = filtered.filter(e => e.status === statusFilter);
|
|
6310
|
+
}
|
|
6311
|
+
// Filter by since timestamp
|
|
6312
|
+
if (sinceFilter) {
|
|
6313
|
+
const sinceTime = new Date(sinceFilter).getTime();
|
|
6314
|
+
if (!isNaN(sinceTime)) {
|
|
6315
|
+
filtered = filtered.filter(e => new Date(e.timestamp).getTime() >= sinceTime);
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
const total = filtered.length;
|
|
6319
|
+
// Return newest first
|
|
6320
|
+
const reversed = [...filtered].reverse();
|
|
6321
|
+
const page = reversed.slice(offset, offset + limit);
|
|
6322
|
+
// Compute summary stats
|
|
6323
|
+
const totalAllowed = filtered.filter(e => e.status === 'allowed').length;
|
|
6324
|
+
const totalDenied = filtered.filter(e => e.status === 'denied').length;
|
|
6325
|
+
const totalCredits = filtered.reduce((sum, e) => sum + e.credits, 0);
|
|
6326
|
+
const avgDurationMs = filtered.length > 0
|
|
6327
|
+
? Math.round(filtered.reduce((sum, e) => sum + e.durationMs, 0) / filtered.length)
|
|
6328
|
+
: 0;
|
|
6329
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
6330
|
+
res.end(JSON.stringify({
|
|
6331
|
+
total,
|
|
6332
|
+
offset,
|
|
6333
|
+
limit,
|
|
6334
|
+
summary: {
|
|
6335
|
+
totalAllowed,
|
|
6336
|
+
totalDenied,
|
|
6337
|
+
totalCredits,
|
|
6338
|
+
avgDurationMs,
|
|
6339
|
+
},
|
|
6340
|
+
requests: page,
|
|
6341
|
+
}));
|
|
6342
|
+
}
|
|
5964
6343
|
}
|
|
5965
6344
|
exports.PayGateServer = PayGateServer;
|
|
5966
6345
|
//# sourceMappingURL=server.js.map
|