agent-smith/dist-in/middleware/usageTracking.js
2026-02-26 19:41:09 +01:00

264 lines
18 KiB
JavaScript

import { supabase } from '../commons/supabase.js';
import { logger } from '../commons/logger.js';
import { FunctionRegistry } from '../commons/registry.js';
/**
* Middleware to track API usage for billing and monitoring
* Tracks request start and updates with completion status
*/
export async function usageTrackingMiddleware(c, next) {
const startTime = Date.now();
// Extract user ID from context (set by auth middleware)
const userId = c.get('userId');
// Skip tracking for unauthenticated requests
if (!userId) {
logger.trace('[UsageTracking] Skipping - No userId');
await next();
return;
}
// Determine product and action
const path = c.req.path;
const method = c.req.method;
// Use Registry to find config
const config = FunctionRegistry.findByRoute(path, method);
const product = config?.productId;
const action = config?.actionId;
logger.trace(`[UsageTracking] Identified: product=${product}, action=${action}`);
// Skip if not a tracked endpoint
if (!product || !action || !config) {
logger.info('[UsageTracking] Skipping - Not a tracked endpoint');
await next();
return;
}
// Generate a job ID for this request
const jobId = `${product}_${action}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Create initial usage record with 'processing' status
let usageId = null;
try {
const { data, error } = await supabase
.from('api_usage')
.insert({
user_id: userId,
endpoint: path,
method,
product,
action,
status: 'processing',
job_id: jobId,
cancellable: config.cancellable || false,
cost_units: config.costUnits,
metadata: {
query: c.req.query(),
userAgent: c.req.header('user-agent'),
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),
},
})
.select('id')
.single();
if (error) {
logger.error({ err: error }, '[UsageTracking] Error creating usage record');
}
else if (data) {
logger.trace(`[UsageTracking] Created usage record: ${data.id}`);
usageId = data.id;
// Store usage ID in context for potential use in handlers
c.set('usageId', usageId);
c.set('jobId', jobId);
}
else {
logger.trace('[UsageTracking] No data returned from insert');
}
}
catch (err) {
logger.error({ err }, 'Failed to create usage record');
}
// Execute the request
let requestError = null;
try {
await next();
}
catch (err) {
requestError = err;
throw err; // Re-throw to let error handler deal with it
}
finally {
// Update usage record with completion status
const endTime = Date.now();
const responseTime = endTime - startTime;
if (usageId) {
// Check if handler requested to skip status update (e.g. for background jobs)
const skipUpdate = c.get('skipUsageStatusUpdate');
if (!skipUpdate) {
updateUsageRecord({
usageId,
responseStatus: c.res.status,
responseTimeMs: responseTime,
error: requestError,
}).catch(err => {
logger.error({ err }, 'Failed to update usage record');
});
}
}
}
}
/**
* Update usage record with completion status
*/
export async function updateUsageRecord(data) {
const status = data.error
? 'failed'
: (data.responseStatus >= 200 && data.responseStatus < 300)
? 'completed'
: 'failed';
const updateData = {
status,
response_status: data.responseStatus,
response_time_ms: data.responseTimeMs,
};
if (data.error) {
updateData.error_message = data.error.message;
}
const { error } = await supabase
.from('api_usage')
.update(updateData)
.eq('id', data.usageId);
if (error) {
logger.error({ err: error }, 'Error updating usage record');
}
}
/**
* Helper function to manually track usage (for non-middleware scenarios)
*/
export async function trackUsage(data) {
try {
const { data: record, error } = await supabase
.from('api_usage')
.insert({
user_id: data.userId,
endpoint: data.endpoint,
method: data.method,
product: data.product,
action: data.action,
status: data.responseStatus ? 'completed' : 'processing',
job_id: data.jobId,
cancellable: data.cancellable,
response_status: data.responseStatus,
response_time_ms: data.responseTimeMs,
cost_units: data.costUnits,
metadata: data.metadata,
api_key_id: data.apiKeyId,
})
.select('id')
.single();
if (error) {
logger.error({ err: error }, 'Error tracking usage');
return null;
}
return record?.id || null;
}
catch (err) {
logger.error({ err }, 'Failed to track usage');
return null;
}
}
/**
* Cancel a job by job ID
*/
export async function cancelJob(userId, jobId) {
try {
const { data, error } = await supabase
.from('api_usage')
.update({
status: 'cancelled',
})
.eq('user_id', userId)
.eq('job_id', jobId)
.eq('cancellable', true)
.in('status', ['pending', 'processing'])
.select('id');
if (error) {
logger.error({ err: error }, 'Error cancelling job');
return false;
}
return !!data && data.length > 0;
}
catch (err) {
logger.error({ err }, 'Failed to cancel job');
return false;
}
}
/**
* Get active (cancellable) jobs for a user
*/
export async function getActiveJobs(userId) {
try {
const { data, error } = await supabase
.from('api_usage')
.select('id, job_id, product, action, status, created_at, metadata')
.eq('user_id', userId)
.eq('cancellable', true)
.in('status', ['pending', 'processing'])
.order('created_at', { ascending: false });
if (error) {
logger.error({ err: error }, 'Error fetching active jobs');
return [];
}
return data || [];
}
catch (err) {
logger.error({ err }, 'Failed to fetch active jobs');
return [];
}
}
/**
* Pause a job by job ID
*/
export async function pauseJob(userId, jobId) {
try {
const { data, error } = await supabase
.from('api_usage')
.update({
status: 'paused',
})
.eq('user_id', userId)
.eq('job_id', jobId)
.eq('cancellable', true)
.eq('status', 'processing') // Only processing jobs can be paused
.select('id');
if (error) {
logger.error({ err: error }, 'Error pausing job');
return false;
}
return !!data && data.length > 0;
}
catch (err) {
logger.error({ err }, 'Failed to pause job');
return false;
}
}
/**
* Resume a paused job by job ID
*/
export async function resumeJob(userId, jobId) {
try {
const { data, error } = await supabase
.from('api_usage')
.update({
status: 'processing',
})
.eq('user_id', userId)
.eq('job_id', jobId)
.eq('cancellable', true)
.eq('status', 'paused') // Only paused jobs can be resumed
.select('id');
if (error) {
logger.error({ err: error }, 'Error resuming job');
return false;
}
return !!data && data.length > 0;
}
catch (err) {
logger.error({ err }, 'Failed to resume job');
return false;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXNhZ2VUcmFja2luZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9taWRkbGV3YXJlL3VzYWdlVHJhY2tpbmcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQ2xELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUM5QyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQWlCMUQ7OztHQUdHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSx1QkFBdUIsQ0FBQyxDQUFVLEVBQUUsSUFBVTtJQUNoRSxNQUFNLFNBQVMsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7SUFFN0Isd0RBQXdEO0lBQ3hELE1BQU0sTUFBTSxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDL0IsNkNBQTZDO0lBQzdDLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUNWLE1BQU0sQ0FBQyxLQUFLLENBQUMsc0NBQXNDLENBQUMsQ0FBQztRQUNyRCxNQUFNLElBQUksRUFBRSxDQUFDO1FBQ2IsT0FBTztJQUNYLENBQUM7SUFFRCwrQkFBK0I7SUFDL0IsTUFBTSxJQUFJLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUM7SUFDeEIsTUFBTSxNQUFNLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUM7SUFFNUIsOEJBQThCO0lBQzlCLE1BQU0sTUFBTSxHQUFHLGdCQUFnQixDQUFDLFdBQVcsQ0FBQyxJQUFJLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDMUQsTUFBTSxPQUFPLEdBQUcsTUFBTSxFQUFFLFNBQVMsQ0FBQztJQUNsQyxNQUFNLE1BQU0sR0FBRyxNQUFNLEVBQUUsUUFBUSxDQUFDO0lBRWhDLE1BQU0sQ0FBQyxLQUFLLENBQUMsdUNBQXVDLE9BQU8sWUFBWSxNQUFNLEVBQUUsQ0FBQyxDQUFDO0lBRWpGLGlDQUFpQztJQUNqQyxJQUFJLENBQUMsT0FBTyxJQUFJLENBQUMsTUFBTSxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7UUFDakMsTUFBTSxDQUFDLElBQUksQ0FBQyxtREFBbUQsQ0FBQyxDQUFDO1FBQ2pFLE1BQU0sSUFBSSxFQUFFLENBQUM7UUFDYixPQUFPO0lBQ1gsQ0FBQztJQUVELHFDQUFxQztJQUNyQyxNQUFNLEtBQUssR0FBRyxHQUFHLE9BQU8sSUFBSSxNQUFNLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxJQUFJLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDO0lBRTlGLHVEQUF1RDtJQUN2RCxJQUFJLE9BQU8sR0FBa0IsSUFBSSxDQUFDO0lBQ2xDLElBQUksQ0FBQztRQUNELE1BQU0sRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsTUFBTSxRQUFRO2FBQ2pDLElBQUksQ0FBQyxXQUFXLENBQUM7YUFDakIsTUFBTSxDQUFDO1lBQ0osT0FBTyxFQUFFLE1BQU07WUFDZixRQUFRLEVBQUUsSUFBSTtZQUNkLE1BQU07WUFDTixPQUFPO1lBQ1AsTUFBTTtZQUNOLE1BQU0sRUFBRSxZQUFZO1lBQ3BCLE1BQU0sRUFBRSxLQUFLO1lBQ2IsV0FBVyxFQUFFLE1BQU0sQ0FBQyxXQUFXLElBQUksS0FBSztZQUN4QyxVQUFVLEVBQUUsTUFBTSxDQUFDLFNBQVM7WUFDNUIsUUFBUSxFQUFFO2dCQUNOLEtBQUssRUFBRSxDQUFDLENBQUMsR0FBRyxDQUFDLEtBQUssRUFBRTtnQkFDcEIsU0FBUyxFQUFFLENBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLFlBQVksQ0FBQztnQkFDckMsRUFBRSxFQUFFLENBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLGlCQUFpQixDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsV0FBVyxDQUFDO2FBQ25FO1NBQ0osQ0FBQzthQUNELE1BQU0sQ0FBQyxJQUFJLENBQUM7YUFDWixNQUFNLEVBQUUsQ0FBQztRQUVkLElBQUksS0FBSyxFQUFFLENBQUM7WUFDUixNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxFQUFFLEtBQUssRUFBRSxFQUFFLDZDQUE2QyxDQUFDLENBQUM7UUFDaEYsQ0FBQzthQUFNLElBQUksSUFBSSxFQUFFLENBQUM7WUFDZCxNQUFNLENBQUMsS0FBSyxDQUFDLHlDQUF5QyxJQUFJLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQztZQUNqRSxPQUFPLEdBQUcsSUFBSSxDQUFDLEVBQUUsQ0FBQztZQUNsQiwwREFBMEQ7WUFDMUQsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxTQUFTLEVBQUUsT0FBTyxDQUFDLENBQUM7WUFDMUIsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDMUIsQ0FBQzthQUFNLENBQUM7WUFDSixNQUFNLENBQUMsS0FBSyxDQUFDLDhDQUE4QyxDQUFDLENBQUM7UUFDakUsQ0FBQztJQUNMLENBQUM7SUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO1FBQ1gsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLEdBQUcsRUFBRSxFQUFFLCtCQUErQixDQUFDLENBQUM7SUFDM0QsQ0FBQztJQUVELHNCQUFzQjtJQUN0QixJQUFJLFlBQVksR0FBaUIsSUFBSSxDQUFDO0lBQ3RDLElBQUksQ0FBQztRQUNELE1BQU0sSUFBSSxFQUFFLENBQUM7SUFDakIsQ0FBQztJQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7UUFDWCxZQUFZLEdBQUcsR0FBWSxDQUFDO1FBQzVCLE1BQU0sR0FBRyxDQUFDLENBQUUsNkNBQTZDO0lBQzdELENBQUM7WUFBUyxDQUFDO1FBQ1AsNkNBQTZDO1FBQzdDLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztRQUMzQixNQUFNLFlBQVksR0FBRyxPQUFPLEdBQUcsU0FBUyxDQUFDO1FBRXpDLElBQUksT0FBTyxFQUFFLENBQUM7WUFDViw4RUFBOEU7WUFDOUUsTUFBTSxVQUFVLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyx1QkFBdUIsQ0FBQyxDQUFDO1lBRWxELElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztnQkFDZCxpQkFBaUIsQ0FBQztvQkFDZCxPQUFPO29CQUNQLGNBQWMsRUFBRSxDQUFDLENBQUMsR0FBRyxDQUFDLE1BQU07b0JBQzVCLGNBQWMsRUFBRSxZQUFZO29CQUM1QixLQUFLLEVBQUUsWUFBWTtpQkFDdEIsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsRUFBRTtvQkFDWCxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxFQUFFLEVBQUUsK0JBQStCLENBQUMsQ0FBQztnQkFDM0QsQ0FBQyxDQUFDLENBQUM7WUFDUCxDQUFDO1FBQ0wsQ0FBQztJQUNMLENBQUM7QUFDTCxDQUFDO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLGlCQUFpQixDQUFDLElBS3ZDO0lBQ0csTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLEtBQUs7UUFDckIsQ0FBQyxDQUFDLFFBQVE7UUFDVixDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsY0FBYyxJQUFJLEdBQUcsSUFBSSxJQUFJLENBQUMsY0FBYyxHQUFHLEdBQUcsQ0FBQztZQUN2RCxDQUFDLENBQUMsV0FBVztZQUNiLENBQUMsQ0FBQyxRQUFRLENBQUM7SUFFbkIsTUFBTSxVQUFVLEdBQVE7UUFDcEIsTUFBTTtRQUNOLGVBQWUsRUFBRSxJQUFJLENBQUMsY0FBYztRQUNwQyxnQkFBZ0IsRUFBRSxJQUFJLENBQUMsY0FBYztLQUN4QyxDQUFDO0lBRUYsSUFBSSxJQUFJLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDYixVQUFVLENBQUMsYUFBYSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDO0lBQ2xELENBQUM7SUFFRCxNQUFNLEVBQUUsS0FBSyxFQUFFLEdBQUcsTUFBTSxRQUFRO1NBQzNCLElBQUksQ0FBQyxXQUFXLENBQUM7U0FDakIsTUFBTSxDQUFDLFVBQVUsQ0FBQztTQUNsQixFQUFFLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUU1QixJQUFJLEtBQUssRUFBRSxDQUFDO1FBQ1IsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsRUFBRSw2QkFBNkIsQ0FBQyxDQUFDO0lBQ2hFLENBQUM7QUFDTCxDQUFDO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLFVBQVUsQ0FBQyxJQUFlO0lBQzVDLElBQUksQ0FBQztRQUNELE1BQU0sRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxHQUFHLE1BQU0sUUFBUTthQUN6QyxJQUFJLENBQUMsV0FBVyxDQUFDO2FBQ2pCLE1BQU0sQ0FBQztZQUNKLE9BQU8sRUFBRSxJQUFJLENBQUMsTUFBTTtZQUNwQixRQUFRLEVBQUUsSUFBSSxDQUFDLFFBQVE7WUFDdkIsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNO1lBQ25CLE9BQU8sRUFBRSxJQUFJLENBQUMsT0FBTztZQUNyQixNQUFNLEVBQUUsSUFBSSxDQUFDLE1BQU07WUFDbkIsTUFBTSxFQUFFLElBQUksQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUMsWUFBWTtZQUN4RCxNQUFNLEVBQUUsSUFBSSxDQUFDLEtBQUs7WUFDbEIsV0FBVyxFQUFFLElBQUksQ0FBQyxXQUFXO1lBQzdCLGVBQWUsRUFBRSxJQUFJLENBQUMsY0FBYztZQUNwQyxnQkFBZ0IsRUFBRSxJQUFJLENBQUMsY0FBYztZQUNyQyxVQUFVLEVBQUUsSUFBSSxDQUFDLFNBQVM7WUFDMUIsUUFBUSxFQUFFLElBQUksQ0FBQyxRQUFRO1lBQ3ZCLFVBQVUsRUFBRSxJQUFJLENBQUMsUUFBUTtTQUM1QixDQUFDO2FBQ0QsTUFBTSxDQUFDLElBQUksQ0FBQzthQUNaLE1BQU0sRUFBRSxDQUFDO1FBRWQsSUFBSSxLQUFLLEVBQUUsQ0FBQztZQUNSLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxHQUFHLEVBQUUsS0FBSyxFQUFFLEVBQUUsc0JBQXNCLENBQUMsQ0FBQztZQUNyRCxPQUFPLElBQUksQ0FBQztRQUNoQixDQUFDO1FBRUQsT0FBTyxNQUFNLEVBQUUsRUFBRSxJQUFJLElBQUksQ0FBQztJQUM5QixDQUFDO0lBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztRQUNYLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFBRSx1QkFBdUIsQ0FBQyxDQUFDO1FBQy9DLE9BQU8sSUFBSSxDQUFDO0lBQ2hCLENBQUM7QUFDTCxDQUFDO0FBQ0Q7O0dBRUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLFNBQVMsQ0FBQyxNQUFjLEVBQUUsS0FBYTtJQUN6RCxJQUFJLENBQUM7UUFDRCxNQUFNLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxHQUFHLE1BQU0sUUFBUTthQUNqQyxJQUFJLENBQUMsV0FBVyxDQUFDO2FBQ2pCLE1BQU0sQ0FBQztZQUNKLE1BQU0sRUFBRSxXQUFXO1NBQ3RCLENBQUM7YUFDRCxFQUFFLENBQUMsU0FBUyxFQUFFLE1BQU0sQ0FBQzthQUNyQixFQUFFLENBQUMsUUFBUSxFQUFFLEtBQUssQ0FBQzthQUNuQixFQUFFLENBQUMsYUFBYSxFQUFFLElBQUksQ0FBQzthQUN2QixFQUFFLENBQUMsUUFBUSxFQUFFLENBQUMsU0FBUyxFQUFFLFlBQVksQ0FBQyxDQUFDO2FBQ3ZDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUVsQixJQUFJLEtBQUssRUFBRSxDQUFDO1lBQ1IsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsRUFBRSxzQkFBc0IsQ0FBQyxDQUFDO1lBQ3JELE9BQU8sS0FBSyxDQUFDO1FBQ2pCLENBQUM7UUFFRCxPQUFPLENBQUMsQ0FBQyxJQUFJLElBQUksSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDckMsQ0FBQztJQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7UUFDWCxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxFQUFFLEVBQUUsc0JBQXNCLENBQUMsQ0FBQztRQUM5QyxPQUFPLEtBQUssQ0FBQztJQUNqQixDQUFDO0FBQ0wsQ0FBQztBQUVEOztHQUVHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxhQUFhLENBQUMsTUFBYztJQUM5QyxJQUFJLENBQUM7UUFDRCxNQUFNLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxHQUFHLE1BQU0sUUFBUTthQUNqQyxJQUFJLENBQUMsV0FBVyxDQUFDO2FBQ2pCLE1BQU0sQ0FBQywyREFBMkQsQ0FBQzthQUNuRSxFQUFFLENBQUMsU0FBUyxFQUFFLE1BQU0sQ0FBQzthQUNyQixFQUFFLENBQUMsYUFBYSxFQUFFLElBQUksQ0FBQzthQUN2QixFQUFFLENBQUMsUUFBUSxFQUFFLENBQUMsU0FBUyxFQUFFLFlBQVksQ0FBQyxDQUFDO2FBQ3ZDLEtBQUssQ0FBQyxZQUFZLEVBQUUsRUFBRSxTQUFTLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztRQUUvQyxJQUFJLEtBQUssRUFBRSxDQUFDO1lBQ1IsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsRUFBRSw0QkFBNEIsQ0FBQyxDQUFDO1lBQzNELE9BQU8sRUFBRSxDQUFDO1FBQ2QsQ0FBQztRQUVELE9BQU8sSUFBSSxJQUFJLEVBQUUsQ0FBQztJQUN0QixDQUFDO0lBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztRQUNYLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFBRSw2QkFBNkIsQ0FBQyxDQUFDO1FBQ3JELE9BQU8sRUFBRSxDQUFDO0lBQ2QsQ0FBQztBQUNMLENBQUM7QUFFRDs7R0FFRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsUUFBUSxDQUFDLE1BQWMsRUFBRSxLQUFhO0lBQ3hELElBQUksQ0FBQztRQUNELE1BQU0sRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsTUFBTSxRQUFRO2FBQ2pDLElBQUksQ0FBQyxXQUFXLENBQUM7YUFDakIsTUFBTSxDQUFDO1lBQ0osTUFBTSxFQUFFLFFBQVE7U0FDbkIsQ0FBQzthQUNELEVBQUUsQ0FBQyxTQUFTLEVBQUUsTUFBTSxDQUFDO2FBQ3JCLEVBQUUsQ0FBQyxRQUFRLEVBQUUsS0FBSyxDQUFDO2FBQ25CLEVBQUUsQ0FBQyxhQUFhLEVBQUUsSUFBSSxDQUFDO2FBQ3ZCLEVBQUUsQ0FBQyxRQUFRLEVBQUUsWUFBWSxDQUFDLENBQUMscUNBQXFDO2FBQ2hFLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUVsQixJQUFJLEtBQUssRUFBRSxDQUFDO1lBQ1IsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsRUFBRSxtQkFBbUIsQ0FBQyxDQUFDO1lBQ2xELE9BQU8sS0FBSyxDQUFDO1FBQ2pCLENBQUM7UUFFRCxPQUFPLENBQUMsQ0FBQyxJQUFJLElBQUksSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDckMsQ0FBQztJQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7UUFDWCxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxFQUFFLEVBQUUscUJBQXFCLENBQUMsQ0FBQztRQUM3QyxPQUFPLEtBQUssQ0FBQztJQUNqQixDQUFDO0FBQ0wsQ0FBQztBQUVEOztHQUVHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxTQUFTLENBQUMsTUFBYyxFQUFFLEtBQWE7SUFDekQsSUFBSSxDQUFDO1FBQ0QsTUFBTSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsR0FBRyxNQUFNLFFBQVE7YUFDakMsSUFBSSxDQUFDLFdBQVcsQ0FBQzthQUNqQixNQUFNLENBQUM7WUFDSixNQUFNLEVBQUUsWUFBWTtTQUN2QixDQUFDO2FBQ0QsRUFBRSxDQUFDLFNBQVMsRUFBRSxNQUFNLENBQUM7YUFDckIsRUFBRSxDQUFDLFFBQVEsRUFBRSxLQUFLLENBQUM7YUFDbkIsRUFBRSxDQUFDLGFBQWEsRUFBRSxJQUFJLENBQUM7YUFDdkIsRUFBRSxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQyxrQ0FBa0M7YUFDekQsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBRWxCLElBQUksS0FBSyxFQUFFLENBQUM7WUFDUixNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxFQUFFLEtBQUssRUFBRSxFQUFFLG9CQUFvQixDQUFDLENBQUM7WUFDbkQsT0FBTyxLQUFLLENBQUM7UUFDakIsQ0FBQztRQUVELE9BQU8sQ0FBQyxDQUFDLElBQUksSUFBSSxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQztJQUNyQyxDQUFDO0lBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztRQUNYLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFBRSxzQkFBc0IsQ0FBQyxDQUFDO1FBQzlDLE9BQU8sS0FBSyxDQUFDO0lBQ2pCLENBQUM7QUFDTCxDQUFDIn0=