@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
275 lines (274 loc) • 9.41 kB
JavaScript
;
/**
* Proof Batch Queue
*
* Collects proofs in memory and submits them in batches to KTA and AgentShield.
* This prevents blocking tool execution while ensuring proofs are eventually submitted.
*
* Performance:
* - Batch size: 10 proofs (configurable)
* - Flush interval: 5 seconds (configurable)
* - Fire-and-forget submission (doesn't block tool execution)
*
* Retry Strategy:
* - Exponential backoff: 1s, 2s, 4s, 8s, 16s
* - Max retries: 5
* - Failed proofs logged and dropped after max retries
*
* Related: PHASE_1_XMCP_I_SERVER.md Epic 3 (Proof Batching)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProofBatchQueue = exports.AgentShieldProofDestination = exports.KTAProofDestination = void 0;
exports.createProofBatchQueue = createProofBatchQueue;
/**
* KTA proof submission destination
*/
class KTAProofDestination {
name = 'KTA';
apiUrl;
apiKey;
constructor(apiUrl, apiKey) {
this.apiUrl = apiUrl.replace(/\/$/, '');
this.apiKey = apiKey;
}
async submit(proofs) {
const headers = {
'Content-Type': 'application/json',
};
if (this.apiKey) {
headers['X-API-Key'] = this.apiKey;
}
const response = await fetch(`${this.apiUrl}/api/v1/proofs/batch`, {
method: 'POST',
headers,
body: JSON.stringify({ proofs }),
});
if (!response.ok) {
throw new Error(`KTA proof submission failed: ${response.status} ${response.statusText}`);
}
}
}
exports.KTAProofDestination = KTAProofDestination;
/**
* AgentShield proof submission destination
*
* Submits proofs to AgentShield's /api/v1/bouncer/proofs endpoint
* with proper authentication and session grouping.
*/
class AgentShieldProofDestination {
name = 'AgentShield';
apiUrl;
apiKey;
constructor(apiUrl, apiKey) {
this.apiUrl = apiUrl.replace(/\/$/, '');
this.apiKey = apiKey;
}
async submit(proofs) {
if (proofs.length === 0) {
return;
}
// Extract session_id from first proof for AgentShield session grouping
// AgentShield uses this for analytics and detection monitoring
const sessionId = proofs[0]?.meta?.sessionId || 'unknown';
// AgentShield API format requires delegation_id and session_id wrapper
const requestBody = {
delegation_id: null, // null for proofs without delegation context
session_id: sessionId, // AgentShield session grouping (same as meta.sessionId)
proofs: proofs
};
const response = await fetch(`${this.apiUrl}/api/v1/bouncer/proofs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`, // Bearer token format
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
// Include response body in error for debugging
const errorBody = await response.text().catch(() => 'Unable to read error body');
throw new Error(`AgentShield proof submission failed: ${response.status} ${response.statusText}\n${errorBody}`);
}
}
}
exports.AgentShieldProofDestination = AgentShieldProofDestination;
/**
* Proof Batch Queue
*
* Collects proofs and submits them in batches to multiple destinations
*/
class ProofBatchQueue {
queue = [];
pendingBatches = [];
config;
flushTimer;
retryTimer;
closed = false;
// Stats
stats = {
queued: 0,
submitted: 0,
failed: 0,
batchesSubmitted: 0,
};
constructor(config) {
this.config = {
destinations: config.destinations,
maxBatchSize: config.maxBatchSize || 10,
flushIntervalMs: config.flushIntervalMs || 5000,
maxRetries: config.maxRetries || 5,
debug: config.debug || false,
};
// Start flush timer
this.startFlushTimer();
// Start retry timer (check every second)
this.startRetryTimer();
}
/**
* Add proof to queue
*/
enqueue(proof) {
if (this.closed) {
console.warn('[ProofBatchQueue] Queue is closed, dropping proof');
return;
}
this.queue.push(proof);
this.stats.queued++;
if (this.config.debug) {
console.log(`[ProofBatchQueue] Enqueued proof (queue size: ${this.queue.length})`);
}
// Flush immediately if batch size reached
if (this.queue.length >= this.config.maxBatchSize) {
this.flush();
}
}
/**
* Flush queue immediately (submit all queued proofs)
*/
async flush() {
if (this.queue.length === 0) {
return;
}
const proofs = this.queue.splice(0, this.config.maxBatchSize);
if (this.config.debug) {
console.log(`[ProofBatchQueue] Flushing ${proofs.length} proofs to ${this.config.destinations.length} destinations`);
}
// Submit to each destination (fire-and-forget)
for (const destination of this.config.destinations) {
const batch = {
proofs,
destination,
retryCount: 0,
};
this.submitBatch(batch); // Fire-and-forget
}
}
/**
* Submit batch to destination (with retries)
*/
async submitBatch(batch) {
try {
await batch.destination.submit(batch.proofs);
this.stats.submitted += batch.proofs.length;
this.stats.batchesSubmitted++;
if (this.config.debug) {
console.log(`[ProofBatchQueue] Successfully submitted ${batch.proofs.length} proofs to ${batch.destination.name}`);
}
}
catch (error) {
console.error(`[ProofBatchQueue] Failed to submit to ${batch.destination.name}:`, error);
// Retry with exponential backoff
if (batch.retryCount < this.config.maxRetries) {
batch.retryCount++;
const backoffMs = Math.min(1000 * Math.pow(2, batch.retryCount - 1), 16000);
batch.nextRetryAt = Date.now() + backoffMs;
this.pendingBatches.push(batch);
if (this.config.debug) {
console.log(`[ProofBatchQueue] Scheduling retry ${batch.retryCount}/${this.config.maxRetries} in ${backoffMs}ms`);
}
}
else {
// Max retries exceeded, drop batch
this.stats.failed += batch.proofs.length;
console.error(`[ProofBatchQueue] Max retries exceeded for ${batch.destination.name}, dropping ${batch.proofs.length} proofs`);
}
}
}
/**
* Start flush timer
*/
startFlushTimer() {
this.flushTimer = setInterval(() => {
if (this.queue.length > 0) {
this.flush();
}
}, this.config.flushIntervalMs);
// Prevent timer from keeping process alive
if (typeof this.flushTimer.unref === 'function') {
this.flushTimer.unref();
}
}
/**
* Start retry timer
*/
startRetryTimer() {
this.retryTimer = setInterval(() => {
const now = Date.now();
// Find batches ready for retry
const retryBatches = this.pendingBatches.filter((batch) => batch.nextRetryAt && batch.nextRetryAt <= now);
if (retryBatches.length > 0) {
// Remove from pending
this.pendingBatches = this.pendingBatches.filter((batch) => !retryBatches.includes(batch));
// Retry each batch
for (const batch of retryBatches) {
this.submitBatch(batch); // Fire-and-forget
}
}
}, 1000);
// Prevent timer from keeping process alive
if (typeof this.retryTimer.unref === 'function') {
this.retryTimer.unref();
}
}
/**
* Close queue and flush remaining proofs
*/
async close() {
this.closed = true;
// Clear timers
if (this.flushTimer) {
clearInterval(this.flushTimer);
}
if (this.retryTimer) {
clearInterval(this.retryTimer);
}
// Flush remaining proofs
await this.flush();
// Wait for pending retries (with timeout)
const maxWaitMs = 30000; // 30 seconds
const startTime = Date.now();
while (this.pendingBatches.length > 0 && Date.now() - startTime < maxWaitMs) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (this.pendingBatches.length > 0) {
console.warn(`[ProofBatchQueue] Closing with ${this.pendingBatches.length} pending batches (timed out)`);
}
}
/**
* Get queue statistics
*/
getStats() {
return {
...this.stats,
queueSize: this.queue.length,
pendingBatches: this.pendingBatches.length,
};
}
}
exports.ProofBatchQueue = ProofBatchQueue;
/**
* Create proof batch queue from config
*/
function createProofBatchQueue(config) {
return new ProofBatchQueue(config);
}