247 lines
7.3 KiB
TypeScript
247 lines
7.3 KiB
TypeScript
const OPERATION_TIMING_SCHEMA_VERSION = 'ltjt-operation-timing-v1';
|
|
const CODE_PATTERN = /^[a-z][a-z0-9_.:-]{0,79}$/i;
|
|
const MAX_STAGES = 64;
|
|
const MAX_DETAILS = 48;
|
|
const MAX_COUNTERS = 24;
|
|
const MAX_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
const ALLOWED_STAGE_CODES = new Set([
|
|
'auto_executor',
|
|
'automation_disabled',
|
|
'batch_fallback',
|
|
'batch_fallback_error',
|
|
'browser_dry_run',
|
|
'browser_execution',
|
|
'cancelled',
|
|
'capability_check',
|
|
'erp_keepalive_degraded',
|
|
'erp_resolution',
|
|
'execution_gate',
|
|
'execution_timeout',
|
|
'extension_error',
|
|
'lifecycle_business_blocked',
|
|
'lifecycle_completed',
|
|
'lifecycle_live',
|
|
'lifecycle_live_submit',
|
|
'lifecycle_preflight',
|
|
'lifecycle_reconciliation',
|
|
'lifecycle_route_preparation',
|
|
'live_submit',
|
|
'open_order_form',
|
|
'open_team_batch_form',
|
|
'post_resolution_execution_gate',
|
|
'preflight',
|
|
'queued',
|
|
'reconciliation',
|
|
'unknown_stage',
|
|
'validation',
|
|
'verification',
|
|
'wait_order_form',
|
|
'wait_team_batch_form'
|
|
]);
|
|
const ALLOWED_STATUSES = new Set([
|
|
'accepted',
|
|
'blocked',
|
|
'cancelled',
|
|
'completed',
|
|
'dry_run',
|
|
'execution_uncertain',
|
|
'failed',
|
|
'mixed',
|
|
'reconciliation_pending',
|
|
'running',
|
|
'saved_unverified',
|
|
'skipped',
|
|
'unknown'
|
|
]);
|
|
const ALLOWED_ACTION_CODES = new Set([
|
|
'cleanup_completed_lifecycle_dialog',
|
|
'create_split_child_live',
|
|
'create_split_parent_live',
|
|
'export_confirmation_sources',
|
|
'inspect_operation_context',
|
|
'live_submit_approved',
|
|
'live_submit_lifecycle_operation',
|
|
'live_submit_team_batch_approved',
|
|
'open_order_form',
|
|
'open_team_batch_form',
|
|
'ping_order_frame',
|
|
'ping_team_batch_frame',
|
|
'preflight_lifecycle_operation',
|
|
'preflight_raw_instruction',
|
|
'preflight_team_batch_native',
|
|
'prepare_lifecycle_operation',
|
|
'resolve_lifecycle_operation',
|
|
'return_to_order_list',
|
|
'unknown_action',
|
|
'verify_lifecycle_operation',
|
|
'verify_order_marker',
|
|
'verify_team_batch_receipts'
|
|
]);
|
|
const ALLOWED_DETAIL_CODES = new Set([
|
|
'executor.erp_keepalive_setup',
|
|
'preflight_customer_restore',
|
|
'preflight_lookup.customer',
|
|
'preflight_lookup.parallel_wall',
|
|
'preflight_lookup.product',
|
|
'preflight_lookup.route',
|
|
'preflight_lookup.staff',
|
|
'preflight_product_linkage',
|
|
'preflight_submit_intercept',
|
|
'result_publish.prepare',
|
|
'storage_prune.local_history',
|
|
'storage_queue.accept_execution',
|
|
'storage_queue.execution_state',
|
|
'storage_queue.result_state',
|
|
'storage_read.accept_execution',
|
|
'storage_read.automation_setting',
|
|
'storage_read.execution_record',
|
|
'storage_read.execution_state',
|
|
'storage_read.result_state',
|
|
'storage_read.task_payload',
|
|
'storage_write.accept_execution',
|
|
'storage_write.execution_state',
|
|
'storage_write.last_frame',
|
|
'storage_write.last_live_submit',
|
|
'storage_write.last_preflight',
|
|
'storage_write.last_return_to_list',
|
|
'storage_write.last_verification',
|
|
'storage_write.result_state',
|
|
'tab_lookup.cache_hit',
|
|
'tab_lookup.create',
|
|
'tab_lookup.find_or_open',
|
|
'tab_lookup.host_permission',
|
|
'tab_lookup.query',
|
|
'tab_lookup.wait_ready'
|
|
]);
|
|
const ALLOWED_COUNTERS = new Set([
|
|
'cache_hit_count',
|
|
'call_count',
|
|
'created_count',
|
|
'execution_count',
|
|
'failure_count',
|
|
'frame_count',
|
|
'protected_root_count',
|
|
'quiet_wait_ms',
|
|
'removed_execution_count',
|
|
'removed_result_count',
|
|
'removed_root_count',
|
|
'removed_task_count',
|
|
'removed_timing_count',
|
|
'request_count',
|
|
'result_count',
|
|
'retained_root_count',
|
|
'sample_count',
|
|
'stable_sample_count',
|
|
'submit_count',
|
|
'tab_count',
|
|
'task_count',
|
|
'timing_count'
|
|
]);
|
|
|
|
function record(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: null;
|
|
}
|
|
|
|
function code(value: unknown, fallback = ''): string {
|
|
const normalized = String(value || '').trim();
|
|
return CODE_PATTERN.test(normalized) ? normalized : fallback;
|
|
}
|
|
|
|
function statusCode(value: unknown, fallback = 'unknown'): string {
|
|
const normalized = code(value);
|
|
return ALLOWED_STATUSES.has(normalized) ? normalized : fallback;
|
|
}
|
|
|
|
function stageCode(value: unknown): string {
|
|
const normalized = code(value);
|
|
return ALLOWED_STAGE_CODES.has(normalized) ? normalized : '';
|
|
}
|
|
|
|
function stepCode(value: unknown): string {
|
|
const normalized = code(value);
|
|
if (normalized === 'script_injection') return normalized;
|
|
if (ALLOWED_DETAIL_CODES.has(normalized)) return normalized;
|
|
const matched = normalized.match(/^(action_roundtrip|action_runtime|network_wait)\.(.+)$/u);
|
|
return matched && ALLOWED_ACTION_CODES.has(matched[2]) ? normalized : '';
|
|
}
|
|
|
|
function duration(value: unknown): number {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number) || number <= 0) return 0;
|
|
return Math.min(MAX_DURATION_MS, Math.round(number));
|
|
}
|
|
|
|
function count(value: unknown): number {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number) || number <= 0) return 0;
|
|
return Math.min(Number.MAX_SAFE_INTEGER, Math.round(number));
|
|
}
|
|
|
|
function sanitizeCounters(value: unknown): Record<string, number> | null {
|
|
const input = record(value);
|
|
if (!input) return null;
|
|
const output: Record<string, number> = {};
|
|
for (const [rawKey, rawValue] of Object.entries(input).slice(0, MAX_COUNTERS)) {
|
|
const key = code(rawKey);
|
|
const number = Number(rawValue);
|
|
if (!ALLOWED_COUNTERS.has(key) || !Number.isFinite(number) || number < 0) continue;
|
|
output[key] = Math.min(Number.MAX_SAFE_INTEGER, Math.round(number));
|
|
}
|
|
return Object.keys(output).length ? output : null;
|
|
}
|
|
|
|
function sanitizeDetail(value: unknown): Record<string, unknown> | null {
|
|
const input = record(value);
|
|
if (!input) return null;
|
|
const step = stepCode(input.step);
|
|
if (!step) return null;
|
|
const counters = sanitizeCounters(input.counters);
|
|
return {
|
|
step,
|
|
status: statusCode(input.status),
|
|
duration_ms: duration(input.duration_ms),
|
|
...(counters ? { counters } : {})
|
|
};
|
|
}
|
|
|
|
function sanitizeStage(value: unknown): Record<string, unknown> | null {
|
|
const input = record(value);
|
|
if (!input) return null;
|
|
const stage = stageCode(input.stage);
|
|
if (!stage) return null;
|
|
const details = (Array.isArray(input.details) ? input.details : [])
|
|
.map(sanitizeDetail)
|
|
.filter((detail): detail is Record<string, unknown> => Boolean(detail))
|
|
.slice(0, MAX_DETAILS);
|
|
const truncatedDetailCount = count(input.truncated_detail_count);
|
|
return {
|
|
stage,
|
|
status: statusCode(input.status),
|
|
duration_ms: duration(input.duration_ms),
|
|
...(details.length ? { details } : {}),
|
|
...(truncatedDetailCount ? { truncated_detail_count: truncatedDetailCount } : {})
|
|
};
|
|
}
|
|
|
|
export function sanitizeOperationTiming(value: unknown): Record<string, unknown> | null {
|
|
const input = record(value);
|
|
if (!input || input.schema_version !== OPERATION_TIMING_SCHEMA_VERSION) return null;
|
|
const stages = (Array.isArray(input.stages) ? input.stages : [])
|
|
.map(sanitizeStage)
|
|
.filter((stage): stage is Record<string, unknown> => Boolean(stage))
|
|
.slice(0, MAX_STAGES);
|
|
const truncatedStageCount = count(input.truncated_stage_count);
|
|
return {
|
|
schema_version: OPERATION_TIMING_SCHEMA_VERSION,
|
|
status: statusCode(input.status),
|
|
total_ms: duration(input.total_ms),
|
|
stage_count: Math.max(stages.length, count(input.stage_count)),
|
|
stages,
|
|
...(truncatedStageCount ? { truncated_stage_count: truncatedStageCount } : {})
|
|
};
|
|
}
|
|
|
|
export { OPERATION_TIMING_SCHEMA_VERSION };
|