paygate-mcp 9.2.0 → 9.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 +14 -4
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/compliance.d.ts +64 -0
- package/dist/compliance.d.ts.map +1 -0
- package/dist/compliance.js +239 -0
- package/dist/compliance.js.map +1 -0
- package/dist/gate.d.ts +13 -2
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +78 -3
- package/dist/gate.js.map +1 -1
- package/dist/guardrails.d.ts +153 -0
- package/dist/guardrails.d.ts.map +1 -0
- package/dist/guardrails.js +347 -0
- package/dist/guardrails.js.map +1 -0
- package/dist/http-proxy.d.ts +1 -1
- package/dist/http-proxy.d.ts.map +1 -1
- package/dist/http-proxy.js +2 -2
- package/dist/http-proxy.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +2 -2
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +3 -3
- package/dist/proxy.js.map +1 -1
- package/dist/router.d.ts +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1 -1
- package/dist/router.js.map +1 -1
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +565 -4
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +21 -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
|
@@ -100,6 +100,8 @@ const stripe_checkout_1 = require("./stripe-checkout");
|
|
|
100
100
|
const backup_1 = require("./backup");
|
|
101
101
|
const response_cache_1 = require("./response-cache");
|
|
102
102
|
const circuit_breaker_1 = require("./circuit-breaker");
|
|
103
|
+
const compliance_1 = require("./compliance");
|
|
104
|
+
const guardrails_1 = require("./guardrails");
|
|
103
105
|
/** Max request body size: 1MB */
|
|
104
106
|
const MAX_BODY_SIZE = 1_048_576;
|
|
105
107
|
/**
|
|
@@ -431,6 +433,8 @@ class PayGateServer {
|
|
|
431
433
|
responseCache = null;
|
|
432
434
|
/** Circuit breaker for backend failure detection (null if disabled) */
|
|
433
435
|
circuitBreaker = null;
|
|
436
|
+
/** Content guardrails for PII detection and content policy enforcement. */
|
|
437
|
+
guardrails;
|
|
434
438
|
/** The active request handler — either proxy or router */
|
|
435
439
|
get handler() {
|
|
436
440
|
return (this.router || this.proxy);
|
|
@@ -654,6 +658,12 @@ class PayGateServer {
|
|
|
654
658
|
cooldownSeconds: this.config.circuitBreakerCooldownSeconds ?? 30,
|
|
655
659
|
});
|
|
656
660
|
}
|
|
661
|
+
// Content guardrails for PII detection
|
|
662
|
+
this.guardrails = new guardrails_1.ContentGuardrails({
|
|
663
|
+
enabled: this.config.guardrails?.enabled ?? false,
|
|
664
|
+
includeContext: this.config.guardrails?.includeContext ?? false,
|
|
665
|
+
maxViolations: this.config.guardrails?.maxViolations ?? 10_000,
|
|
666
|
+
});
|
|
657
667
|
// Backup manager for full state snapshots
|
|
658
668
|
this.backup = new backup_1.BackupManager(this.createBackupProvider());
|
|
659
669
|
}
|
|
@@ -911,7 +921,7 @@ class PayGateServer {
|
|
|
911
921
|
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
912
922
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, HEAD, OPTIONS');
|
|
913
923
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, X-Admin-Key, Mcp-Session-Id, Authorization, X-Request-Id');
|
|
914
|
-
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Credits-Remaining, X-Request-Id, X-PayGate-Version, X-Cache');
|
|
924
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Credits-Remaining, X-Request-Id, X-PayGate-Version, X-Cache, X-Output-Surcharge');
|
|
915
925
|
if (corsConfig?.credentials) {
|
|
916
926
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
917
927
|
}
|
|
@@ -1109,6 +1119,8 @@ class PayGateServer {
|
|
|
1109
1119
|
return this.handleCreditTransfer(req, res);
|
|
1110
1120
|
case '/keys/bulk':
|
|
1111
1121
|
return this.handleBulkOperations(req, res);
|
|
1122
|
+
case '/keys/webhook':
|
|
1123
|
+
return this.handleKeyWebhook(req, res);
|
|
1112
1124
|
case '/keys/export':
|
|
1113
1125
|
return this.handleKeyExport(req, res);
|
|
1114
1126
|
case '/keys/import':
|
|
@@ -1380,6 +1392,17 @@ class PayGateServer {
|
|
|
1380
1392
|
return this.handleComplianceReport(req, res);
|
|
1381
1393
|
this.sendError(res, 405, 'Method not allowed. Use GET.');
|
|
1382
1394
|
return;
|
|
1395
|
+
case '/admin/compliance/export':
|
|
1396
|
+
if (req.method === 'GET')
|
|
1397
|
+
return this.handleComplianceExport(req, res);
|
|
1398
|
+
this.sendError(res, 405, 'Method not allowed. Use GET.');
|
|
1399
|
+
return;
|
|
1400
|
+
case '/admin/guardrails':
|
|
1401
|
+
return this.handleGuardrails(req, res);
|
|
1402
|
+
case '/admin/guardrails/violations':
|
|
1403
|
+
return this.handleGuardrailViolations(req, res);
|
|
1404
|
+
case '/keys/geo':
|
|
1405
|
+
return this.handleKeyGeo(req, res);
|
|
1383
1406
|
case '/admin/sla':
|
|
1384
1407
|
if (req.method === 'GET')
|
|
1385
1408
|
return this.handleSlaMonitoring(req, res);
|
|
@@ -1765,6 +1788,9 @@ class PayGateServer {
|
|
|
1765
1788
|
}
|
|
1766
1789
|
// Extract client IP for IP allowlist checking (trusted proxy-aware)
|
|
1767
1790
|
const clientIp = resolveClientIp(req, this.config.trustedProxies);
|
|
1791
|
+
// Extract country code from reverse proxy header (Cloudflare, AWS, etc.)
|
|
1792
|
+
const geoHeader = this.config.geoCountryHeader || 'X-Country';
|
|
1793
|
+
const countryCode = req.headers[geoHeader.toLowerCase()] || undefined;
|
|
1768
1794
|
// Extract scoped token tool restrictions (set by resolveApiKey)
|
|
1769
1795
|
const scopedTokenTools = req._scopedTokenTools;
|
|
1770
1796
|
// ─── Batch tool calls ────────────────────────────────────────────────
|
|
@@ -1782,7 +1808,7 @@ class PayGateServer {
|
|
|
1782
1808
|
res.end(JSON.stringify(errResp));
|
|
1783
1809
|
return;
|
|
1784
1810
|
}
|
|
1785
|
-
const batchResponse = await this.handler.handleBatchRequest(calls, request.id, apiKey, clientIp, scopedTokenTools);
|
|
1811
|
+
const batchResponse = await this.handler.handleBatchRequest(calls, request.id, apiKey, clientIp, scopedTokenTools, countryCode);
|
|
1786
1812
|
// Audit + metrics for batch
|
|
1787
1813
|
if (batchResponse.error) {
|
|
1788
1814
|
this.audit.log('gate.deny', (0, audit_1.maskKeyForAudit)(apiKey || 'anonymous'), `Batch denied (${calls.length} calls)`, {
|
|
@@ -1837,6 +1863,38 @@ class PayGateServer {
|
|
|
1837
1863
|
const toolCallStartTime = Date.now();
|
|
1838
1864
|
let cacheHit = false;
|
|
1839
1865
|
let circuitBroken = false;
|
|
1866
|
+
// ─── Content Guardrails: scan inputs ────────────────────────────
|
|
1867
|
+
if (this.guardrails.isEnabled && pluginRequest.method === 'tools/call') {
|
|
1868
|
+
const grToolName = pluginRequest.params?.name || '';
|
|
1869
|
+
const grToolArgs = pluginRequest.params?.arguments;
|
|
1870
|
+
const inputContent = grToolArgs ? JSON.stringify(grToolArgs) : '';
|
|
1871
|
+
if (inputContent) {
|
|
1872
|
+
const grResult = this.guardrails.check(inputContent, grToolName, 'input', apiKey ? apiKey.slice(0, 10) : 'anon');
|
|
1873
|
+
if (grResult.blocked) {
|
|
1874
|
+
const ruleNames = grResult.violations.filter(v => v.action === 'block').map(v => v.ruleName).join(', ');
|
|
1875
|
+
this.audit.log('guardrail.block', (0, audit_1.maskKeyForAudit)(apiKey || 'anonymous'), `Input blocked by guardrail: ${ruleNames}`, { tool: grToolName, scope: 'input', requestId });
|
|
1876
|
+
const errResp = {
|
|
1877
|
+
jsonrpc: '2.0',
|
|
1878
|
+
id: pluginRequest.id,
|
|
1879
|
+
error: { code: -32406, message: `Content policy violation: input blocked by guardrail (${ruleNames})` },
|
|
1880
|
+
};
|
|
1881
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId });
|
|
1882
|
+
res.end(JSON.stringify(errResp));
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (grResult.redactedContent && grToolArgs) {
|
|
1886
|
+
// Replace arguments with redacted version
|
|
1887
|
+
try {
|
|
1888
|
+
pluginRequest.params.arguments = JSON.parse(grResult.redactedContent);
|
|
1889
|
+
}
|
|
1890
|
+
catch { /* ignore parse error, use original */ }
|
|
1891
|
+
}
|
|
1892
|
+
// Add warnings as response headers
|
|
1893
|
+
for (const warning of grResult.warnings) {
|
|
1894
|
+
res.setHeader('X-Guardrail-Warning', warning);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1840
1898
|
// ─── Response Cache check (before forwarding to backend) ────────
|
|
1841
1899
|
// response is guaranteed assigned before use: either via cache hit, circuit breaker, or handler
|
|
1842
1900
|
let response = null;
|
|
@@ -1885,12 +1943,12 @@ class PayGateServer {
|
|
|
1885
1943
|
}, timeoutMs);
|
|
1886
1944
|
});
|
|
1887
1945
|
response = await Promise.race([
|
|
1888
|
-
this.handler.handleRequest(pluginRequest, apiKey, clientIp, scopedTokenTools),
|
|
1946
|
+
this.handler.handleRequest(pluginRequest, apiKey, clientIp, scopedTokenTools, countryCode),
|
|
1889
1947
|
timeoutPromise,
|
|
1890
1948
|
]);
|
|
1891
1949
|
}
|
|
1892
1950
|
else {
|
|
1893
|
-
response = await this.handler.handleRequest(pluginRequest, apiKey, clientIp, scopedTokenTools);
|
|
1951
|
+
response = await this.handler.handleRequest(pluginRequest, apiKey, clientIp, scopedTokenTools, countryCode);
|
|
1894
1952
|
}
|
|
1895
1953
|
// Record circuit breaker outcome
|
|
1896
1954
|
if (pluginRequest.method === 'tools/call' && this.circuitBreaker) {
|
|
@@ -1910,6 +1968,64 @@ class PayGateServer {
|
|
|
1910
1968
|
}
|
|
1911
1969
|
}
|
|
1912
1970
|
}
|
|
1971
|
+
// ─── Outcome-based pricing: post-call output surcharge ────────
|
|
1972
|
+
let outputSurcharge = 0;
|
|
1973
|
+
if (!cacheHit && !circuitBroken && request.method === 'tools/call' && response && !response.error && apiKey) {
|
|
1974
|
+
const toolName = request.params?.name || '';
|
|
1975
|
+
const toolPricingForOutput = this.config.toolPricing[toolName];
|
|
1976
|
+
const creditsPerKbOutput = toolPricingForOutput?.creditsPerKbOutput ?? 0;
|
|
1977
|
+
if (creditsPerKbOutput > 0) {
|
|
1978
|
+
const outputBytes = Buffer.byteLength(JSON.stringify(response.result ?? ''), 'utf-8');
|
|
1979
|
+
const outputKb = outputBytes / 1024;
|
|
1980
|
+
outputSurcharge = Math.ceil(outputKb * creditsPerKbOutput);
|
|
1981
|
+
if (outputSurcharge > 0) {
|
|
1982
|
+
const keyRecord = this.gate.store.getKey(apiKey);
|
|
1983
|
+
if (keyRecord && keyRecord.credits >= outputSurcharge) {
|
|
1984
|
+
this.gate.store.deductCredits(apiKey, outputSurcharge);
|
|
1985
|
+
keyRecord.totalSpent += outputSurcharge;
|
|
1986
|
+
this.gate.store.save();
|
|
1987
|
+
this.gate.onCreditsDeducted?.(apiKey, outputSurcharge);
|
|
1988
|
+
}
|
|
1989
|
+
else if (keyRecord) {
|
|
1990
|
+
// Insufficient credits for output surcharge — deduct what's available
|
|
1991
|
+
const available = keyRecord.credits;
|
|
1992
|
+
if (available > 0) {
|
|
1993
|
+
this.gate.store.deductCredits(apiKey, available);
|
|
1994
|
+
keyRecord.totalSpent += available;
|
|
1995
|
+
this.gate.store.save();
|
|
1996
|
+
this.gate.onCreditsDeducted?.(apiKey, available);
|
|
1997
|
+
}
|
|
1998
|
+
outputSurcharge = available;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
// ─── Content Guardrails: scan outputs ────────────────────────────
|
|
2004
|
+
if (this.guardrails.isEnabled && request.method === 'tools/call' && response && !response.error) {
|
|
2005
|
+
const grToolName = request.params?.name || '';
|
|
2006
|
+
const outputContent = response.result ? JSON.stringify(response.result) : '';
|
|
2007
|
+
if (outputContent) {
|
|
2008
|
+
const grResult = this.guardrails.check(outputContent, grToolName, 'output', apiKey ? apiKey.slice(0, 10) : 'anon');
|
|
2009
|
+
if (grResult.blocked) {
|
|
2010
|
+
const ruleNames = grResult.violations.filter(v => v.action === 'block').map(v => v.ruleName).join(', ');
|
|
2011
|
+
this.audit.log('guardrail.block', (0, audit_1.maskKeyForAudit)(apiKey || 'anonymous'), `Output blocked by guardrail: ${ruleNames}`, { tool: grToolName, scope: 'output', requestId });
|
|
2012
|
+
response = {
|
|
2013
|
+
jsonrpc: '2.0',
|
|
2014
|
+
id: response.id,
|
|
2015
|
+
error: { code: -32406, message: `Content policy violation: output blocked by guardrail (${ruleNames})` },
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
else if (grResult.redactedContent) {
|
|
2019
|
+
try {
|
|
2020
|
+
response = { ...response, result: JSON.parse(grResult.redactedContent) };
|
|
2021
|
+
}
|
|
2022
|
+
catch { /* ignore parse error, use original */ }
|
|
2023
|
+
}
|
|
2024
|
+
for (const warning of grResult.warnings) {
|
|
2025
|
+
res.setHeader('X-Guardrail-Warning', warning);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
1913
2029
|
// Plugin: afterToolCall — let plugins modify the response
|
|
1914
2030
|
if (this.plugins.count > 0 && request.method === 'tools/call') {
|
|
1915
2031
|
const toolName = request.params?.name || '';
|
|
@@ -2026,6 +2142,10 @@ class PayGateServer {
|
|
|
2026
2142
|
if (request.method === 'tools/call' && this.responseCache) {
|
|
2027
2143
|
rateLimitHeaders['X-Cache'] = cacheHit ? 'HIT' : 'MISS';
|
|
2028
2144
|
}
|
|
2145
|
+
// Add X-Output-Surcharge header for outcome-based pricing
|
|
2146
|
+
if (request.method === 'tools/call' && outputSurcharge > 0) {
|
|
2147
|
+
rateLimitHeaders['X-Output-Surcharge'] = String(outputSurcharge);
|
|
2148
|
+
}
|
|
2029
2149
|
// Check if client accepts SSE
|
|
2030
2150
|
const accept = req.headers['accept'] || '';
|
|
2031
2151
|
const wantsSse = accept.includes('text/event-stream');
|
|
@@ -2413,6 +2533,11 @@ class PayGateServer {
|
|
|
2413
2533
|
adminRestore: 'POST /admin/restore — Restore state from backup snapshot (requires X-Admin-Key, admin)',
|
|
2414
2534
|
adminCache: 'GET /admin/cache — Response cache stats; DELETE /admin/cache?tool= — Clear cache (requires X-Admin-Key)',
|
|
2415
2535
|
adminCircuit: 'GET /admin/circuit — Circuit breaker status; POST /admin/circuit — Reset circuit breaker (requires X-Admin-Key)',
|
|
2536
|
+
adminComplianceExport: 'GET /admin/compliance/export?framework=soc2|gdpr|hipaa&format=json|csv — Framework-specific compliance audit export with event classification (requires X-Admin-Key)',
|
|
2537
|
+
keyWebhook: 'POST /keys/webhook — Set per-key webhook URL; DELETE /keys/webhook — Remove per-key webhook (requires X-Admin-Key)',
|
|
2538
|
+
adminGuardrails: 'GET /admin/guardrails — View guardrail rules and stats; POST /admin/guardrails — Toggle, upsert rule, or import rules; DELETE /admin/guardrails?id= — Remove rule (requires X-Admin-Key)',
|
|
2539
|
+
adminGuardrailViolations: 'GET /admin/guardrails/violations — View content policy violations with filters; DELETE — Clear all violations (requires X-Admin-Key)',
|
|
2540
|
+
keyGeo: 'POST /keys/geo — Set per-key country restrictions; GET /keys/geo?key= — View geo restrictions; DELETE /keys/geo?key= — Clear restrictions (requires X-Admin-Key)',
|
|
2416
2541
|
...(this.oauth ? {
|
|
2417
2542
|
oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
|
|
2418
2543
|
oauthRegister: 'POST /oauth/register — Register OAuth client',
|
|
@@ -3044,6 +3169,54 @@ class PayGateServer {
|
|
|
3044
3169
|
});
|
|
3045
3170
|
break;
|
|
3046
3171
|
}
|
|
3172
|
+
case 'suspend': {
|
|
3173
|
+
const key = op.key;
|
|
3174
|
+
if (!key) {
|
|
3175
|
+
results.push({ index: i, action: 'suspend', success: false, error: 'Missing key' });
|
|
3176
|
+
break;
|
|
3177
|
+
}
|
|
3178
|
+
const rec = this.gate.store.getKey(key);
|
|
3179
|
+
if (!rec) {
|
|
3180
|
+
results.push({ index: i, action: 'suspend', success: false, error: 'Key not found' });
|
|
3181
|
+
break;
|
|
3182
|
+
}
|
|
3183
|
+
if (rec.suspended) {
|
|
3184
|
+
results.push({ index: i, action: 'suspend', success: true, result: { keyMasked: (0, audit_1.maskKeyForAudit)(key), alreadySuspended: true } });
|
|
3185
|
+
break;
|
|
3186
|
+
}
|
|
3187
|
+
rec.suspended = true;
|
|
3188
|
+
this.gate.store.save();
|
|
3189
|
+
if (this.redisSync)
|
|
3190
|
+
this.redisSync.saveKey(rec).catch(() => { });
|
|
3191
|
+
this.audit.log('key.suspended', 'admin', 'Key suspended (bulk)', { keyMasked: (0, audit_1.maskKeyForAudit)(key) });
|
|
3192
|
+
this.emitWebhookAdmin('key.suspended', 'admin', { keyMasked: (0, audit_1.maskKeyForAudit)(key) });
|
|
3193
|
+
results.push({ index: i, action: 'suspend', success: true, result: { keyMasked: (0, audit_1.maskKeyForAudit)(key) } });
|
|
3194
|
+
break;
|
|
3195
|
+
}
|
|
3196
|
+
case 'resume': {
|
|
3197
|
+
const key = op.key;
|
|
3198
|
+
if (!key) {
|
|
3199
|
+
results.push({ index: i, action: 'resume', success: false, error: 'Missing key' });
|
|
3200
|
+
break;
|
|
3201
|
+
}
|
|
3202
|
+
const rec = this.gate.store.getKey(key);
|
|
3203
|
+
if (!rec) {
|
|
3204
|
+
results.push({ index: i, action: 'resume', success: false, error: 'Key not found' });
|
|
3205
|
+
break;
|
|
3206
|
+
}
|
|
3207
|
+
if (!rec.suspended) {
|
|
3208
|
+
results.push({ index: i, action: 'resume', success: true, result: { keyMasked: (0, audit_1.maskKeyForAudit)(key), alreadyActive: true } });
|
|
3209
|
+
break;
|
|
3210
|
+
}
|
|
3211
|
+
rec.suspended = false;
|
|
3212
|
+
this.gate.store.save();
|
|
3213
|
+
if (this.redisSync)
|
|
3214
|
+
this.redisSync.saveKey(rec).catch(() => { });
|
|
3215
|
+
this.audit.log('key.resumed', 'admin', 'Key resumed (bulk)', { keyMasked: (0, audit_1.maskKeyForAudit)(key) });
|
|
3216
|
+
this.emitWebhookAdmin('key.resumed', 'admin', { keyMasked: (0, audit_1.maskKeyForAudit)(key) });
|
|
3217
|
+
results.push({ index: i, action: 'resume', success: true, result: { keyMasked: (0, audit_1.maskKeyForAudit)(key) } });
|
|
3218
|
+
break;
|
|
3219
|
+
}
|
|
3047
3220
|
default:
|
|
3048
3221
|
results.push({ index: i, action: op.action || 'unknown', success: false, error: `Unknown action: ${op.action}` });
|
|
3049
3222
|
}
|
|
@@ -3063,6 +3236,116 @@ class PayGateServer {
|
|
|
3063
3236
|
});
|
|
3064
3237
|
}
|
|
3065
3238
|
// ─── /keys/export — Export all API keys for backup ────────────────────────
|
|
3239
|
+
// ─── /keys/webhook — Per-key webhook URL management ────────────────────────
|
|
3240
|
+
async handleKeyWebhook(req, res) {
|
|
3241
|
+
if (!this.checkAdmin(req, res))
|
|
3242
|
+
return;
|
|
3243
|
+
if (req.method === 'POST') {
|
|
3244
|
+
// Set per-key webhook URL
|
|
3245
|
+
const body = await this.readBody(req);
|
|
3246
|
+
let params;
|
|
3247
|
+
try {
|
|
3248
|
+
params = JSON.parse(body);
|
|
3249
|
+
}
|
|
3250
|
+
catch {
|
|
3251
|
+
this.sendError(res, 400, 'Invalid JSON');
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
const apiKeyPrefix = params.apiKey;
|
|
3255
|
+
const webhookUrl = params.webhookUrl;
|
|
3256
|
+
const webhookSecret = params.webhookSecret;
|
|
3257
|
+
if (!apiKeyPrefix) {
|
|
3258
|
+
this.sendError(res, 400, 'Missing apiKey parameter');
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (!webhookUrl) {
|
|
3262
|
+
this.sendError(res, 400, 'Missing webhookUrl parameter');
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
// SSRF check
|
|
3266
|
+
const ssrfError = (0, webhook_1.checkSsrf)(webhookUrl);
|
|
3267
|
+
if (ssrfError) {
|
|
3268
|
+
this.sendError(res, 400, `Webhook URL blocked: ${ssrfError}`);
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
// Find key
|
|
3272
|
+
const allKeys = this.gate.store.getAllRecords();
|
|
3273
|
+
const keyRecord = allKeys.find((k) => k.key.startsWith(apiKeyPrefix));
|
|
3274
|
+
if (!keyRecord) {
|
|
3275
|
+
this.sendError(res, 404, 'API key not found');
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
keyRecord.webhookUrl = webhookUrl;
|
|
3279
|
+
if (webhookSecret !== undefined) {
|
|
3280
|
+
keyRecord.webhookSecret = webhookSecret;
|
|
3281
|
+
}
|
|
3282
|
+
this.gate.store.save();
|
|
3283
|
+
// Reset cached emitter so next event creates fresh one
|
|
3284
|
+
this.gate.removeKeyWebhook(keyRecord.key);
|
|
3285
|
+
this.audit.log('key.webhook_updated', 'admin', `Per-key webhook set for ${keyRecord.name}`, {
|
|
3286
|
+
keyPrefix: keyRecord.key.slice(0, 7),
|
|
3287
|
+
webhookUrl: webhookUrl.replace(/\/\/([^:]+):([^@]+)@/, '//$1:***@'), // mask credentials
|
|
3288
|
+
});
|
|
3289
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3290
|
+
res.end(JSON.stringify({
|
|
3291
|
+
ok: true,
|
|
3292
|
+
keyName: keyRecord.name,
|
|
3293
|
+
webhookUrl,
|
|
3294
|
+
webhookSecret: webhookSecret ? '***' : null,
|
|
3295
|
+
}));
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
if (req.method === 'DELETE') {
|
|
3299
|
+
// Remove per-key webhook URL
|
|
3300
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
3301
|
+
const apiKeyPrefix = url.searchParams.get('apiKey');
|
|
3302
|
+
if (!apiKeyPrefix) {
|
|
3303
|
+
this.sendError(res, 400, 'Missing apiKey query parameter');
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
const allKeys2 = this.gate.store.getAllRecords();
|
|
3307
|
+
const keyRecord = allKeys2.find((k) => k.key.startsWith(apiKeyPrefix));
|
|
3308
|
+
if (!keyRecord) {
|
|
3309
|
+
this.sendError(res, 404, 'API key not found');
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
delete keyRecord.webhookUrl;
|
|
3313
|
+
delete keyRecord.webhookSecret;
|
|
3314
|
+
this.gate.store.save();
|
|
3315
|
+
// Destroy cached emitter
|
|
3316
|
+
this.gate.removeKeyWebhook(keyRecord.key);
|
|
3317
|
+
this.audit.log('key.webhook_updated', 'admin', `Per-key webhook removed for ${keyRecord.name}`, {
|
|
3318
|
+
keyPrefix: keyRecord.key.slice(0, 7),
|
|
3319
|
+
});
|
|
3320
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3321
|
+
res.end(JSON.stringify({ ok: true, keyName: keyRecord.name, webhookUrl: null }));
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
if (req.method === 'GET') {
|
|
3325
|
+
// Get per-key webhook status
|
|
3326
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
3327
|
+
const apiKeyPrefix = url.searchParams.get('apiKey');
|
|
3328
|
+
if (!apiKeyPrefix) {
|
|
3329
|
+
this.sendError(res, 400, 'Missing apiKey query parameter');
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
const allKeys3 = this.gate.store.getAllRecords();
|
|
3333
|
+
const keyRecord = allKeys3.find((k) => k.key.startsWith(apiKeyPrefix));
|
|
3334
|
+
if (!keyRecord) {
|
|
3335
|
+
this.sendError(res, 404, 'API key not found');
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3339
|
+
res.end(JSON.stringify({
|
|
3340
|
+
keyName: keyRecord.name,
|
|
3341
|
+
webhookUrl: keyRecord.webhookUrl || null,
|
|
3342
|
+
webhookSecret: keyRecord.webhookSecret ? '***' : null,
|
|
3343
|
+
configured: !!keyRecord.webhookUrl,
|
|
3344
|
+
}));
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
this.sendError(res, 405, 'Method not allowed. Use GET, POST, or DELETE.');
|
|
3348
|
+
}
|
|
3066
3349
|
handleKeyExport(req, res) {
|
|
3067
3350
|
if (req.method !== 'GET') {
|
|
3068
3351
|
this.sendError(res, 405, 'Method not allowed');
|
|
@@ -7192,6 +7475,44 @@ class PayGateServer {
|
|
|
7192
7475
|
generatedAt: new Date().toISOString(),
|
|
7193
7476
|
});
|
|
7194
7477
|
}
|
|
7478
|
+
// ─── /admin/compliance/export — Framework-specific audit export ──────────
|
|
7479
|
+
handleComplianceExport(req, res) {
|
|
7480
|
+
if (!this.checkAdmin(req, res))
|
|
7481
|
+
return;
|
|
7482
|
+
const url = new URL(req.url || '/', `http://localhost`);
|
|
7483
|
+
const framework = (url.searchParams.get('framework') || 'soc2');
|
|
7484
|
+
const format = url.searchParams.get('format') || 'json';
|
|
7485
|
+
const since = url.searchParams.get('since') || undefined;
|
|
7486
|
+
const until = url.searchParams.get('until') || undefined;
|
|
7487
|
+
const validFrameworks = ['soc2', 'gdpr', 'hipaa'];
|
|
7488
|
+
if (!validFrameworks.includes(framework)) {
|
|
7489
|
+
this.sendError(res, 400, `Invalid framework. Must be one of: ${validFrameworks.join(', ')}`);
|
|
7490
|
+
return;
|
|
7491
|
+
}
|
|
7492
|
+
const report = (0, compliance_1.generateComplianceReport)(this.audit, framework, {
|
|
7493
|
+
since,
|
|
7494
|
+
until,
|
|
7495
|
+
serverVersion: PKG_VERSION,
|
|
7496
|
+
});
|
|
7497
|
+
this.audit.log('config.export', 'admin', `Compliance report exported: ${framework} (${format})`, {
|
|
7498
|
+
framework, format, events: report.meta.totalEvents,
|
|
7499
|
+
});
|
|
7500
|
+
if (format === 'csv') {
|
|
7501
|
+
const csv = (0, compliance_1.complianceReportToCsv)(report);
|
|
7502
|
+
res.writeHead(200, {
|
|
7503
|
+
'Content-Type': 'text/csv',
|
|
7504
|
+
'Content-Disposition': `attachment; filename="compliance-${framework}-${new Date().toISOString().slice(0, 10)}.csv"`,
|
|
7505
|
+
});
|
|
7506
|
+
res.end(csv);
|
|
7507
|
+
return;
|
|
7508
|
+
}
|
|
7509
|
+
// JSON (default) with download Content-Disposition
|
|
7510
|
+
res.writeHead(200, {
|
|
7511
|
+
'Content-Type': 'application/json',
|
|
7512
|
+
'Content-Disposition': `attachment; filename="compliance-${framework}-${new Date().toISOString().slice(0, 10)}.json"`,
|
|
7513
|
+
});
|
|
7514
|
+
res.end(JSON.stringify(report, null, 2));
|
|
7515
|
+
}
|
|
7195
7516
|
// ─── /admin/sla — SLA Monitoring ─────────────────────────────────────────
|
|
7196
7517
|
handleSlaMonitoring(req, res) {
|
|
7197
7518
|
if (!this.checkAdmin(req, res))
|
|
@@ -12026,6 +12347,14 @@ class PayGateServer {
|
|
|
12026
12347
|
else if (this.gate.webhook) {
|
|
12027
12348
|
this.gate.webhook.emitAdmin(type, actor, metadata);
|
|
12028
12349
|
}
|
|
12350
|
+
// Per-key webhook: also emit to key-specific webhook if the event references a key
|
|
12351
|
+
const apiKey = (metadata.apiKey || metadata.key);
|
|
12352
|
+
if (apiKey) {
|
|
12353
|
+
const keyWebhook = this.gate.getKeyWebhook(apiKey);
|
|
12354
|
+
if (keyWebhook) {
|
|
12355
|
+
keyWebhook.emitAdmin(type, actor, metadata);
|
|
12356
|
+
}
|
|
12357
|
+
}
|
|
12029
12358
|
}
|
|
12030
12359
|
syncKeyMutation(apiKey) {
|
|
12031
12360
|
if (!this.redisSync)
|
|
@@ -12804,6 +13133,238 @@ class PayGateServer {
|
|
|
12804
13133
|
tools,
|
|
12805
13134
|
});
|
|
12806
13135
|
}
|
|
13136
|
+
// ─── /admin/guardrails — Guardrail rule management ─────────────────────────
|
|
13137
|
+
async handleGuardrails(req, res) {
|
|
13138
|
+
if (req.method === 'GET') {
|
|
13139
|
+
if (!this.checkAdmin(req, res, 'viewer'))
|
|
13140
|
+
return;
|
|
13141
|
+
this.sendJson(res, 200, {
|
|
13142
|
+
enabled: this.guardrails.isEnabled,
|
|
13143
|
+
rules: this.guardrails.getRules(),
|
|
13144
|
+
stats: this.guardrails.getStats(),
|
|
13145
|
+
});
|
|
13146
|
+
return;
|
|
13147
|
+
}
|
|
13148
|
+
if (req.method === 'POST') {
|
|
13149
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
13150
|
+
return;
|
|
13151
|
+
const body = await this.readBody(req);
|
|
13152
|
+
let params;
|
|
13153
|
+
try {
|
|
13154
|
+
params = safeJsonParse(body);
|
|
13155
|
+
}
|
|
13156
|
+
catch {
|
|
13157
|
+
this.sendError(res, 400, 'Invalid JSON');
|
|
13158
|
+
return;
|
|
13159
|
+
}
|
|
13160
|
+
// Toggle enabled/disabled
|
|
13161
|
+
if (params.enabled !== undefined) {
|
|
13162
|
+
this.guardrails.setEnabled(!!params.enabled);
|
|
13163
|
+
this.audit.log('guardrail.toggle', 'admin', `Guardrails ${params.enabled ? 'enabled' : 'disabled'}`);
|
|
13164
|
+
this.sendJson(res, 200, { enabled: this.guardrails.isEnabled });
|
|
13165
|
+
return;
|
|
13166
|
+
}
|
|
13167
|
+
// Upsert a rule
|
|
13168
|
+
if (params.rule && typeof params.rule === 'object') {
|
|
13169
|
+
const rule = params.rule;
|
|
13170
|
+
if (!rule.id || !rule.name || !rule.pattern || !rule.action) {
|
|
13171
|
+
this.sendError(res, 400, 'Rule requires: id, name, pattern, action');
|
|
13172
|
+
return;
|
|
13173
|
+
}
|
|
13174
|
+
if (!['log', 'warn', 'block', 'redact'].includes(rule.action)) {
|
|
13175
|
+
this.sendError(res, 400, 'Invalid action. Must be: log, warn, block, redact');
|
|
13176
|
+
return;
|
|
13177
|
+
}
|
|
13178
|
+
// Validate regex compiles
|
|
13179
|
+
try {
|
|
13180
|
+
new RegExp(rule.pattern, rule.flags ?? 'gi');
|
|
13181
|
+
}
|
|
13182
|
+
catch {
|
|
13183
|
+
this.sendError(res, 400, 'Invalid regex pattern');
|
|
13184
|
+
return;
|
|
13185
|
+
}
|
|
13186
|
+
const saved = this.guardrails.upsertRule({
|
|
13187
|
+
...rule,
|
|
13188
|
+
active: rule.active !== false,
|
|
13189
|
+
scope: rule.scope || 'both',
|
|
13190
|
+
tools: rule.tools || [],
|
|
13191
|
+
});
|
|
13192
|
+
this.audit.log('guardrail.rule_upsert', 'admin', `Guardrail rule upserted: ${saved.name}`, { ruleId: saved.id, action: saved.action });
|
|
13193
|
+
this.sendJson(res, 200, { rule: saved });
|
|
13194
|
+
return;
|
|
13195
|
+
}
|
|
13196
|
+
// Import rules
|
|
13197
|
+
if (params.rules && Array.isArray(params.rules)) {
|
|
13198
|
+
const mode = params.mode === 'replace' ? 'replace' : 'merge';
|
|
13199
|
+
const count = this.guardrails.importRules(params.rules, mode);
|
|
13200
|
+
this.audit.log('guardrail.rules_import', 'admin', `Imported ${count} guardrail rules (${mode})`);
|
|
13201
|
+
this.sendJson(res, 200, { imported: count, mode, totalRules: this.guardrails.getRules().length });
|
|
13202
|
+
return;
|
|
13203
|
+
}
|
|
13204
|
+
this.sendError(res, 400, 'Provide "enabled" (boolean), "rule" (object), or "rules" (array)');
|
|
13205
|
+
return;
|
|
13206
|
+
}
|
|
13207
|
+
if (req.method === 'DELETE') {
|
|
13208
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
13209
|
+
return;
|
|
13210
|
+
const url = new URL(req.url || '', `http://localhost`);
|
|
13211
|
+
const ruleId = url.searchParams.get('id');
|
|
13212
|
+
if (!ruleId) {
|
|
13213
|
+
this.sendError(res, 400, 'Missing ?id= query parameter');
|
|
13214
|
+
return;
|
|
13215
|
+
}
|
|
13216
|
+
const removed = this.guardrails.removeRule(ruleId);
|
|
13217
|
+
if (!removed) {
|
|
13218
|
+
this.sendError(res, 404, `Rule not found: ${ruleId}`);
|
|
13219
|
+
return;
|
|
13220
|
+
}
|
|
13221
|
+
this.audit.log('guardrail.rule_delete', 'admin', `Guardrail rule deleted: ${ruleId}`);
|
|
13222
|
+
this.sendJson(res, 200, { deleted: ruleId });
|
|
13223
|
+
return;
|
|
13224
|
+
}
|
|
13225
|
+
this.sendError(res, 405, 'Method not allowed. Use GET/POST/DELETE.');
|
|
13226
|
+
}
|
|
13227
|
+
// ─── /admin/guardrails/violations — View guardrail violations ──────────────
|
|
13228
|
+
handleGuardrailViolations(req, res) {
|
|
13229
|
+
if (req.method === 'GET') {
|
|
13230
|
+
if (!this.checkAdmin(req, res, 'viewer'))
|
|
13231
|
+
return;
|
|
13232
|
+
const url = new URL(req.url || '', `http://localhost`);
|
|
13233
|
+
const limit = clampInt(parseInt(url.searchParams.get('limit') || '100', 10), 1, 1000);
|
|
13234
|
+
const offset = clampInt(parseInt(url.searchParams.get('offset') || '0', 10), 0, 100_000);
|
|
13235
|
+
const ruleId = url.searchParams.get('ruleId') || undefined;
|
|
13236
|
+
const tool = url.searchParams.get('tool') || undefined;
|
|
13237
|
+
const action = url.searchParams.get('action') || undefined;
|
|
13238
|
+
const since = url.searchParams.get('since') || undefined;
|
|
13239
|
+
const until = url.searchParams.get('until') || undefined;
|
|
13240
|
+
const result = this.guardrails.queryViolations({ ruleId, tool, action, since, until, limit, offset });
|
|
13241
|
+
this.sendJson(res, 200, {
|
|
13242
|
+
violations: result.violations,
|
|
13243
|
+
total: result.total,
|
|
13244
|
+
limit,
|
|
13245
|
+
offset,
|
|
13246
|
+
hasMore: offset + limit < result.total,
|
|
13247
|
+
});
|
|
13248
|
+
return;
|
|
13249
|
+
}
|
|
13250
|
+
if (req.method === 'DELETE') {
|
|
13251
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
13252
|
+
return;
|
|
13253
|
+
const cleared = this.guardrails.clearViolations();
|
|
13254
|
+
this.sendJson(res, 200, { cleared });
|
|
13255
|
+
return;
|
|
13256
|
+
}
|
|
13257
|
+
this.sendError(res, 405, 'Method not allowed. Use GET/DELETE.');
|
|
13258
|
+
}
|
|
13259
|
+
// ─── /keys/geo — Per-key geo-restriction management ─────────────────────────
|
|
13260
|
+
async handleKeyGeo(req, res) {
|
|
13261
|
+
if (!this.checkAdmin(req, res, 'admin'))
|
|
13262
|
+
return;
|
|
13263
|
+
if (req.method === 'GET') {
|
|
13264
|
+
const url = new URL(req.url || '', `http://localhost`);
|
|
13265
|
+
const apiKeyPrefix = url.searchParams.get('key');
|
|
13266
|
+
if (!apiKeyPrefix) {
|
|
13267
|
+
this.sendError(res, 400, 'Missing ?key= query parameter (key prefix)');
|
|
13268
|
+
return;
|
|
13269
|
+
}
|
|
13270
|
+
const record = this.gate.store.getAllRecords().find((k) => k.key.startsWith(apiKeyPrefix));
|
|
13271
|
+
if (!record) {
|
|
13272
|
+
this.sendError(res, 404, 'Key not found');
|
|
13273
|
+
return;
|
|
13274
|
+
}
|
|
13275
|
+
this.sendJson(res, 200, {
|
|
13276
|
+
keyPrefix: record.key.slice(0, 10) + '...',
|
|
13277
|
+
allowedCountries: record.allowedCountries || [],
|
|
13278
|
+
deniedCountries: record.deniedCountries || [],
|
|
13279
|
+
});
|
|
13280
|
+
return;
|
|
13281
|
+
}
|
|
13282
|
+
if (req.method === 'POST') {
|
|
13283
|
+
const body = await this.readBody(req);
|
|
13284
|
+
let params;
|
|
13285
|
+
try {
|
|
13286
|
+
params = safeJsonParse(body);
|
|
13287
|
+
}
|
|
13288
|
+
catch {
|
|
13289
|
+
this.sendError(res, 400, 'Invalid JSON');
|
|
13290
|
+
return;
|
|
13291
|
+
}
|
|
13292
|
+
const apiKeyPrefix = params.key;
|
|
13293
|
+
if (!apiKeyPrefix) {
|
|
13294
|
+
this.sendError(res, 400, 'Missing "key" parameter (key prefix)');
|
|
13295
|
+
return;
|
|
13296
|
+
}
|
|
13297
|
+
const record = this.gate.store.getAllRecords().find((k) => k.key.startsWith(apiKeyPrefix));
|
|
13298
|
+
if (!record) {
|
|
13299
|
+
this.sendError(res, 404, 'Key not found');
|
|
13300
|
+
return;
|
|
13301
|
+
}
|
|
13302
|
+
// Validate country codes (ISO 3166-1 alpha-2)
|
|
13303
|
+
const COUNTRY_CODE_RE = /^[A-Z]{2}$/;
|
|
13304
|
+
if (params.allowedCountries !== undefined) {
|
|
13305
|
+
if (!Array.isArray(params.allowedCountries)) {
|
|
13306
|
+
this.sendError(res, 400, '"allowedCountries" must be an array of ISO 3166-1 alpha-2 codes');
|
|
13307
|
+
return;
|
|
13308
|
+
}
|
|
13309
|
+
const codes = params.allowedCountries.map(c => String(c).toUpperCase());
|
|
13310
|
+
if (codes.some(c => !COUNTRY_CODE_RE.test(c))) {
|
|
13311
|
+
this.sendError(res, 400, 'Invalid country code. Use ISO 3166-1 alpha-2 (e.g., "US", "GB", "DE")');
|
|
13312
|
+
return;
|
|
13313
|
+
}
|
|
13314
|
+
record.allowedCountries = codes.length > 0 ? codes : undefined;
|
|
13315
|
+
}
|
|
13316
|
+
if (params.deniedCountries !== undefined) {
|
|
13317
|
+
if (!Array.isArray(params.deniedCountries)) {
|
|
13318
|
+
this.sendError(res, 400, '"deniedCountries" must be an array of ISO 3166-1 alpha-2 codes');
|
|
13319
|
+
return;
|
|
13320
|
+
}
|
|
13321
|
+
const codes = params.deniedCountries.map(c => String(c).toUpperCase());
|
|
13322
|
+
if (codes.some(c => !COUNTRY_CODE_RE.test(c))) {
|
|
13323
|
+
this.sendError(res, 400, 'Invalid country code. Use ISO 3166-1 alpha-2 (e.g., "US", "GB", "DE")');
|
|
13324
|
+
return;
|
|
13325
|
+
}
|
|
13326
|
+
record.deniedCountries = codes.length > 0 ? codes : undefined;
|
|
13327
|
+
}
|
|
13328
|
+
this.gate.store.save();
|
|
13329
|
+
if (this.redisSync)
|
|
13330
|
+
this.redisSync.saveKey(record).catch(() => { });
|
|
13331
|
+
this.audit.log('key.geo_set', 'admin', `Geo restrictions set for ${record.key.slice(0, 10)}...`, {
|
|
13332
|
+
keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
|
|
13333
|
+
allowedCountries: record.allowedCountries || [],
|
|
13334
|
+
deniedCountries: record.deniedCountries || [],
|
|
13335
|
+
});
|
|
13336
|
+
this.sendJson(res, 200, {
|
|
13337
|
+
keyPrefix: record.key.slice(0, 10) + '...',
|
|
13338
|
+
allowedCountries: record.allowedCountries || [],
|
|
13339
|
+
deniedCountries: record.deniedCountries || [],
|
|
13340
|
+
});
|
|
13341
|
+
return;
|
|
13342
|
+
}
|
|
13343
|
+
if (req.method === 'DELETE') {
|
|
13344
|
+
const url = new URL(req.url || '', `http://localhost`);
|
|
13345
|
+
const apiKeyPrefix = url.searchParams.get('key');
|
|
13346
|
+
if (!apiKeyPrefix) {
|
|
13347
|
+
this.sendError(res, 400, 'Missing ?key= query parameter (key prefix)');
|
|
13348
|
+
return;
|
|
13349
|
+
}
|
|
13350
|
+
const record = this.gate.store.getAllRecords().find((k) => k.key.startsWith(apiKeyPrefix));
|
|
13351
|
+
if (!record) {
|
|
13352
|
+
this.sendError(res, 404, 'Key not found');
|
|
13353
|
+
return;
|
|
13354
|
+
}
|
|
13355
|
+
record.allowedCountries = undefined;
|
|
13356
|
+
record.deniedCountries = undefined;
|
|
13357
|
+
this.gate.store.save();
|
|
13358
|
+
if (this.redisSync)
|
|
13359
|
+
this.redisSync.saveKey(record).catch(() => { });
|
|
13360
|
+
this.audit.log('key.geo_cleared', 'admin', `Geo restrictions cleared for ${record.key.slice(0, 10)}...`, {
|
|
13361
|
+
keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
|
|
13362
|
+
});
|
|
13363
|
+
this.sendJson(res, 200, { keyPrefix: record.key.slice(0, 10) + '...', allowedCountries: [], deniedCountries: [] });
|
|
13364
|
+
return;
|
|
13365
|
+
}
|
|
13366
|
+
this.sendError(res, 405, 'Method not allowed. Use GET/POST/DELETE.');
|
|
13367
|
+
}
|
|
12807
13368
|
/** Calculate percentile from an array of numbers. */
|
|
12808
13369
|
percentile(values, p) {
|
|
12809
13370
|
if (values.length === 0)
|