Proteção XSS, assinatura de webhook, limite de execuções e data no histórico
- Utilitário centralizado Utils.escapeHtml() substituindo duplicações locais - Escaping completo em todos os componentes (agents, tasks, schedules, pipelines, webhooks, terminal, history, tags) - Verificação HMAC-SHA256 para webhooks usando raw body - Limite de 5000 registros no store de execuções (maxSize) - Data de execução visível no histórico com ícone de calendário - Remoção de mutex desnecessário no flush síncrono do db.js - Novos stores preparatórios (secrets, notifications, agentVersions)
This commit is contained in:
@@ -3437,50 +3437,36 @@ tbody tr:hover td {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-card-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-card-date {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-card-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-card-task {
|
.history-card-task {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-card-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-card-date,
|
||||||
.history-card-duration {
|
.history-card-duration {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 5px;
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
flex-shrink: 0;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-card-date i,
|
||||||
.history-card-duration i {
|
.history-card-duration i {
|
||||||
width: 12px;
|
width: 13px;
|
||||||
height: 12px;
|
height: 13px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-card-actions {
|
.history-card-actions {
|
||||||
@@ -3986,12 +3972,6 @@ tbody tr:hover td {
|
|||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-card-duration-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pipeline-step-meta-group {
|
.pipeline-step-meta-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1118,6 +1118,7 @@
|
|||||||
|
|
||||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/utils.js"></script>
|
||||||
<script src="js/components/toast.js"></script>
|
<script src="js/components/toast.js"></script>
|
||||||
<script src="js/components/modal.js"></script>
|
<script src="js/components/modal.js"></script>
|
||||||
<script src="js/components/terminal.js"></script>
|
<script src="js/components/terminal.js"></script>
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ const App = {
|
|||||||
<div class="approval-icon"><i data-lucide="shield-alert"></i></div>
|
<div class="approval-icon"><i data-lucide="shield-alert"></i></div>
|
||||||
<div class="approval-text">
|
<div class="approval-text">
|
||||||
<strong>Aprovação necessária</strong>
|
<strong>Aprovação necessária</strong>
|
||||||
<span>Passo ${stepIndex + 1} (${agentName || 'agente'}) aguardando autorização</span>
|
<span>Passo ${stepIndex + 1} (${Utils.escapeHtml(agentName) || 'agente'}) aguardando autorização</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="approval-actions">
|
<div class="approval-actions">
|
||||||
<button class="btn btn--primary btn--sm" id="approval-approve-btn" type="button">Aprovar</button>
|
<button class="btn btn--primary btn--sm" id="approval-approve-btn" type="button">Aprovar</button>
|
||||||
@@ -661,8 +661,8 @@ const App = {
|
|||||||
hidden.value = JSON.stringify(tags);
|
hidden.value = JSON.stringify(tags);
|
||||||
chips.innerHTML = tags.map((t) => `
|
chips.innerHTML = tags.map((t) => `
|
||||||
<span class="tag-chip">
|
<span class="tag-chip">
|
||||||
${t}
|
${Utils.escapeHtml(t)}
|
||||||
<button type="button" class="tag-remove" data-tag="${t}" aria-label="Remover tag ${t}">×</button>
|
<button type="button" class="tag-remove" data-tag="${Utils.escapeHtml(t)}" aria-label="Remover tag ${Utils.escapeHtml(t)}">×</button>
|
||||||
</span>
|
</span>
|
||||||
`).join('');
|
`).join('');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const AgentsUI = {
|
|||||||
const model = (agent.config && agent.config.model) || agent.model || 'claude-sonnet-4-6';
|
const model = (agent.config && agent.config.model) || agent.model || 'claude-sonnet-4-6';
|
||||||
const updatedAt = AgentsUI.formatDate(agent.updated_at || agent.updatedAt || agent.created_at || agent.createdAt);
|
const updatedAt = AgentsUI.formatDate(agent.updated_at || agent.updatedAt || agent.created_at || agent.createdAt);
|
||||||
const tags = Array.isArray(agent.tags) && agent.tags.length > 0
|
const tags = Array.isArray(agent.tags) && agent.tags.length > 0
|
||||||
? `<div class="agent-tags">${agent.tags.map((t) => `<span class="tag-chip tag-chip--sm">${t}</span>`).join('')}</div>`
|
? `<div class="agent-tags">${agent.tags.map((t) => `<span class="tag-chip tag-chip--sm">${Utils.escapeHtml(t)}</span>`).join('')}</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -87,12 +87,12 @@ const AgentsUI = {
|
|||||||
<span>${initials}</span>
|
<span>${initials}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<h3 class="agent-name">${name}</h3>
|
<h3 class="agent-name">${Utils.escapeHtml(name)}</h3>
|
||||||
<span class="badge ${statusClass}">${statusLabel}</span>
|
<span class="badge ${statusClass}">${statusLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${agent.description ? `<p class="agent-description">${agent.description}</p>` : ''}
|
${agent.description ? `<p class="agent-description">${Utils.escapeHtml(agent.description)}</p>` : ''}
|
||||||
${tags}
|
${tags}
|
||||||
|
|
||||||
<div class="agent-meta">
|
<div class="agent-meta">
|
||||||
@@ -279,7 +279,7 @@ const AgentsUI = {
|
|||||||
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||||
allAgents
|
allAgents
|
||||||
.filter((a) => a.status === 'active')
|
.filter((a) => a.status === 'active')
|
||||||
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
.map((a) => `<option value="${a.id}">${Utils.escapeHtml(a.agent_name || a.name)}</option>`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
selectEl.value = agentId;
|
selectEl.value = agentId;
|
||||||
@@ -311,7 +311,7 @@ const AgentsUI = {
|
|||||||
savedTaskSelect.innerHTML = '<option value="">Digitar manualmente...</option>' +
|
savedTaskSelect.innerHTML = '<option value="">Digitar manualmente...</option>' +
|
||||||
tasks.map((t) => {
|
tasks.map((t) => {
|
||||||
const label = t.category ? `[${t.category.toUpperCase()}] ${t.name}` : t.name;
|
const label = t.category ? `[${t.category.toUpperCase()}] ${t.name}` : t.name;
|
||||||
return `<option value="${t.id}">${label}</option>`;
|
return `<option value="${t.id}">${Utils.escapeHtml(label)}</option>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
AgentsUI._savedTasksCache = tasks;
|
AgentsUI._savedTasksCache = tasks;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ const DashboardUI = {
|
|||||||
list.innerHTML = executions.map((exec) => {
|
list.innerHTML = executions.map((exec) => {
|
||||||
const statusClass = DashboardUI._statusBadgeClass(exec.status);
|
const statusClass = DashboardUI._statusBadgeClass(exec.status);
|
||||||
const statusLabel = DashboardUI._statusLabel(exec.status);
|
const statusLabel = DashboardUI._statusLabel(exec.status);
|
||||||
const name = exec.agentName || exec.pipelineName || exec.agentId || 'Execução';
|
const name = Utils.escapeHtml(exec.agentName || exec.pipelineName || exec.agentId || 'Execução');
|
||||||
const taskText = exec.task || exec.input || '';
|
const taskText = Utils.escapeHtml(exec.task || exec.input || '');
|
||||||
const typeBadge = exec.type === 'pipeline'
|
const typeBadge = exec.type === 'pipeline'
|
||||||
? '<span class="badge badge--purple" style="font-size:0.6rem;padding:1px 5px;">Pipeline</span> '
|
? '<span class="badge badge--purple" style="font-size:0.6rem;padding:1px 5px;">Pipeline</span> '
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
@@ -70,22 +70,21 @@ const HistoryUI = {
|
|||||||
<div class="history-card-header">
|
<div class="history-card-header">
|
||||||
<div class="history-card-identity">
|
<div class="history-card-identity">
|
||||||
${typeBadge}
|
${typeBadge}
|
||||||
<span class="history-card-name">${HistoryUI._escapeHtml(name)}</span>
|
<span class="history-card-name">${Utils.escapeHtml(name)}</span>
|
||||||
</div>
|
|
||||||
<div class="history-card-status">
|
|
||||||
${statusBadge}
|
${statusBadge}
|
||||||
<span class="history-card-date">${date}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-card-meta">
|
<div class="history-card-task">${Utils.escapeHtml(task)}</div>
|
||||||
<span class="history-card-task">${HistoryUI._escapeHtml(task)}</span>
|
<div class="history-card-info">
|
||||||
<span class="history-card-duration-group">
|
<span class="history-card-date">
|
||||||
|
<i data-lucide="calendar" aria-hidden="true"></i>
|
||||||
|
${date}
|
||||||
|
</span>
|
||||||
<span class="history-card-duration">
|
<span class="history-card-duration">
|
||||||
<i data-lucide="clock" aria-hidden="true"></i>
|
<i data-lucide="clock" aria-hidden="true"></i>
|
||||||
${duration}
|
${duration}
|
||||||
</span>
|
</span>
|
||||||
${costHtml}
|
${costHtml}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="history-card-actions">
|
<div class="history-card-actions">
|
||||||
<button class="btn btn-ghost btn-sm" data-action="view-execution" data-id="${exec.id}" type="button">
|
<button class="btn btn-ghost btn-sm" data-action="view-execution" data-id="${exec.id}" type="button">
|
||||||
@@ -195,18 +194,18 @@ const HistoryUI = {
|
|||||||
const endDate = exec.endedAt ? HistoryUI._formatDate(exec.endedAt) : '—';
|
const endDate = exec.endedAt ? HistoryUI._formatDate(exec.endedAt) : '—';
|
||||||
|
|
||||||
const resultBlock = exec.result
|
const resultBlock = exec.result
|
||||||
? `<div class="execution-result" role="region" aria-label="Resultado da execução">${HistoryUI._escapeHtml(exec.result)}</div>`
|
? `<div class="execution-result" role="region" aria-label="Resultado da execução">${Utils.escapeHtml(exec.result)}</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const errorBlock = exec.error
|
const errorBlock = exec.error
|
||||||
? `<div class="execution-result execution-result--error" role="alert">${HistoryUI._escapeHtml(exec.error)}</div>`
|
? `<div class="execution-result execution-result--error" role="alert">${Utils.escapeHtml(exec.error)}</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="execution-detail-meta">
|
<div class="execution-detail-meta">
|
||||||
<div class="execution-detail-row">
|
<div class="execution-detail-row">
|
||||||
<span class="execution-detail-label">Agente</span>
|
<span class="execution-detail-label">Agente</span>
|
||||||
<span class="execution-detail-value">${HistoryUI._escapeHtml(exec.agentName || exec.agentId || '—')}</span>
|
<span class="execution-detail-value">${Utils.escapeHtml(exec.agentName || exec.agentId || '—')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="execution-detail-row">
|
<div class="execution-detail-row">
|
||||||
<span class="execution-detail-label">Status</span>
|
<span class="execution-detail-label">Status</span>
|
||||||
@@ -243,7 +242,7 @@ const HistoryUI = {
|
|||||||
${exec.task ? `
|
${exec.task ? `
|
||||||
<div class="execution-detail-section">
|
<div class="execution-detail-section">
|
||||||
<h3 class="execution-detail-section-title">Tarefa</h3>
|
<h3 class="execution-detail-section-title">Tarefa</h3>
|
||||||
<p class="execution-detail-task">${HistoryUI._escapeHtml(exec.task)}</p>
|
<p class="execution-detail-task">${Utils.escapeHtml(exec.task)}</p>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${resultBlock ? `
|
${resultBlock ? `
|
||||||
<div class="execution-detail-section">
|
<div class="execution-detail-section">
|
||||||
@@ -279,7 +278,7 @@ const HistoryUI = {
|
|||||||
<div class="pipeline-step-detail">
|
<div class="pipeline-step-detail">
|
||||||
<div class="pipeline-step-header">
|
<div class="pipeline-step-header">
|
||||||
<div class="pipeline-step-identity">
|
<div class="pipeline-step-identity">
|
||||||
<span class="pipeline-step-agent">${HistoryUI._escapeHtml(step.agentName || step.agentId || 'Agente')}</span>
|
<span class="pipeline-step-agent">${Utils.escapeHtml(step.agentName || step.agentId || 'Agente')}</span>
|
||||||
${HistoryUI._statusBadge(step.status)}
|
${HistoryUI._statusBadge(step.status)}
|
||||||
</div>
|
</div>
|
||||||
<span class="pipeline-step-meta-group">
|
<span class="pipeline-step-meta-group">
|
||||||
@@ -297,13 +296,13 @@ const HistoryUI = {
|
|||||||
Prompt utilizado
|
Prompt utilizado
|
||||||
</button>
|
</button>
|
||||||
<div class="pipeline-step-prompt-body" hidden>
|
<div class="pipeline-step-prompt-body" hidden>
|
||||||
<div class="execution-result execution-result--prompt">${HistoryUI._escapeHtml(step.prompt)}</div>
|
<div class="execution-result execution-result--prompt">${Utils.escapeHtml(step.prompt)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${step.result ? `
|
${step.result ? `
|
||||||
<div class="pipeline-step-result">
|
<div class="pipeline-step-result">
|
||||||
<span class="pipeline-step-result-label">Resultado</span>
|
<span class="pipeline-step-result-label">Resultado</span>
|
||||||
<div class="execution-result">${HistoryUI._escapeHtml(step.result)}</div>
|
<div class="execution-result">${Utils.escapeHtml(step.result)}</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${step.status === 'error' ? `
|
${step.status === 'error' ? `
|
||||||
<div class="execution-result execution-result--error">Passo falhou.</div>` : ''}
|
<div class="execution-result execution-result--error">Passo falhou.</div>` : ''}
|
||||||
@@ -316,7 +315,7 @@ const HistoryUI = {
|
|||||||
<div class="execution-detail-meta">
|
<div class="execution-detail-meta">
|
||||||
<div class="execution-detail-row">
|
<div class="execution-detail-row">
|
||||||
<span class="execution-detail-label">Pipeline</span>
|
<span class="execution-detail-label">Pipeline</span>
|
||||||
<span class="execution-detail-value">${HistoryUI._escapeHtml(exec.pipelineName || exec.pipelineId || '—')}</span>
|
<span class="execution-detail-value">${Utils.escapeHtml(exec.pipelineName || exec.pipelineId || '—')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="execution-detail-row">
|
<div class="execution-detail-row">
|
||||||
<span class="execution-detail-label">Status</span>
|
<span class="execution-detail-label">Status</span>
|
||||||
@@ -343,7 +342,7 @@ const HistoryUI = {
|
|||||||
${exec.input ? `
|
${exec.input ? `
|
||||||
<div class="execution-detail-section">
|
<div class="execution-detail-section">
|
||||||
<h3 class="execution-detail-section-title">Input Inicial</h3>
|
<h3 class="execution-detail-section-title">Input Inicial</h3>
|
||||||
<p class="execution-detail-task">${HistoryUI._escapeHtml(exec.input)}</p>
|
<p class="execution-detail-task">${Utils.escapeHtml(exec.input)}</p>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${steps.length > 0 ? `
|
${steps.length > 0 ? `
|
||||||
<div class="execution-detail-section">
|
<div class="execution-detail-section">
|
||||||
@@ -355,7 +354,7 @@ const HistoryUI = {
|
|||||||
${exec.error ? `
|
${exec.error ? `
|
||||||
<div class="execution-detail-section">
|
<div class="execution-detail-section">
|
||||||
<h3 class="execution-detail-section-title">Erro</h3>
|
<h3 class="execution-detail-section-title">Erro</h3>
|
||||||
<div class="execution-result execution-result--error">${HistoryUI._escapeHtml(exec.error)}</div>
|
<div class="execution-result execution-result--error">${Utils.escapeHtml(exec.error)}</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
@@ -435,15 +434,6 @@ const HistoryUI = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_escapeHtml(str) {
|
|
||||||
if (!str) return '';
|
|
||||||
return String(str)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.HistoryUI = HistoryUI;
|
window.HistoryUI = HistoryUI;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const PipelinesUI = {
|
|||||||
const stepCount = steps.length;
|
const stepCount = steps.length;
|
||||||
|
|
||||||
const flowHtml = steps.map((step, index) => {
|
const flowHtml = steps.map((step, index) => {
|
||||||
const agentName = step.agentName || step.agentId || 'Agente';
|
const agentName = Utils.escapeHtml(step.agentName || step.agentId || 'Agente');
|
||||||
const isLast = index === steps.length - 1;
|
const isLast = index === steps.length - 1;
|
||||||
const approvalIcon = step.requiresApproval && index > 0
|
const approvalIcon = step.requiresApproval && index > 0
|
||||||
? '<i data-lucide="shield-check" style="width:10px;height:10px;color:var(--warning)"></i> '
|
? '<i data-lucide="shield-check" style="width:10px;height:10px;color:var(--warning)"></i> '
|
||||||
@@ -102,12 +102,12 @@ const PipelinesUI = {
|
|||||||
<div class="agent-card-body">
|
<div class="agent-card-body">
|
||||||
<div class="agent-card-top">
|
<div class="agent-card-top">
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<h3 class="agent-name">${pipeline.name || 'Sem nome'}</h3>
|
<h3 class="agent-name">${Utils.escapeHtml(pipeline.name || 'Sem nome')}</h3>
|
||||||
<span class="badge badge-active">${stepCount} ${stepCount === 1 ? 'passo' : 'passos'}</span>
|
<span class="badge badge-active">${stepCount} ${stepCount === 1 ? 'passo' : 'passos'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${pipeline.description ? `<p class="agent-description">${pipeline.description}</p>` : ''}
|
${pipeline.description ? `<p class="agent-description">${Utils.escapeHtml(pipeline.description)}</p>` : ''}
|
||||||
|
|
||||||
<div class="pipeline-flow">
|
<div class="pipeline-flow">
|
||||||
${flowHtml || '<span class="agent-description">Nenhum passo configurado</span>'}
|
${flowHtml || '<span class="agent-description">Nenhum passo configurado</span>'}
|
||||||
@@ -192,7 +192,7 @@ const PipelinesUI = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agentOptions = PipelinesUI.agents
|
const agentOptions = PipelinesUI.agents
|
||||||
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
.map((a) => `<option value="${a.id}">${Utils.escapeHtml(a.agent_name || a.name)}</option>`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
container.innerHTML = PipelinesUI._steps.map((step, index) => {
|
container.innerHTML = PipelinesUI._steps.map((step, index) => {
|
||||||
@@ -225,7 +225,7 @@ const PipelinesUI = {
|
|||||||
placeholder="{{input}} será substituído pelo output anterior"
|
placeholder="{{input}} será substituído pelo output anterior"
|
||||||
data-step-field="inputTemplate"
|
data-step-field="inputTemplate"
|
||||||
data-step-index="${index}"
|
data-step-index="${index}"
|
||||||
>${step.inputTemplate || ''}</textarea>
|
>${Utils.escapeHtml(step.inputTemplate || '')}</textarea>
|
||||||
${approvalHtml}
|
${approvalHtml}
|
||||||
</div>
|
</div>
|
||||||
<div class="pipeline-step-actions">
|
<div class="pipeline-step-actions">
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ const SchedulesUI = {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${schedule.agentName || '—'}</td>
|
<td>${Utils.escapeHtml(schedule.agentName || '—')}</td>
|
||||||
<td class="schedule-task-cell" title="${schedule.taskDescription || ''}">${schedule.taskDescription || '—'}</td>
|
<td class="schedule-task-cell" title="${Utils.escapeHtml(schedule.taskDescription || '')}">${Utils.escapeHtml(schedule.taskDescription || '—')}</td>
|
||||||
<td>
|
<td>
|
||||||
<code class="font-mono">${cronExpr}</code>
|
<code class="font-mono">${cronExpr}</code>
|
||||||
</td>
|
</td>
|
||||||
@@ -106,7 +106,7 @@ const SchedulesUI = {
|
|||||||
select.innerHTML = '<option value="">Selecionar agente...</option>' +
|
select.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||||
agents
|
agents
|
||||||
.filter((a) => a.status === 'active')
|
.filter((a) => a.status === 'active')
|
||||||
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
.map((a) => `<option value="${a.id}">${Utils.escapeHtml(a.agent_name || a.name)}</option>`)
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +233,12 @@ const SchedulesUI = {
|
|||||||
const duration = SchedulesUI._formatDuration(exec.startedAt, exec.endedAt);
|
const duration = SchedulesUI._formatDuration(exec.startedAt, exec.endedAt);
|
||||||
const cost = exec.costUsd || exec.totalCostUsd || 0;
|
const cost = exec.costUsd || exec.totalCostUsd || 0;
|
||||||
const costStr = cost > 0 ? `$${cost.toFixed(4)}` : '—';
|
const costStr = cost > 0 ? `$${cost.toFixed(4)}` : '—';
|
||||||
const taskStr = SchedulesUI._escapeHtml(SchedulesUI._truncate(exec.task || '', 60));
|
const taskStr = Utils.escapeHtml(Utils.truncate(exec.task || '', 60));
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${SchedulesUI._escapeHtml(exec.agentName || '—')}</td>
|
<td>${Utils.escapeHtml(exec.agentName || '—')}</td>
|
||||||
<td title="${SchedulesUI._escapeHtml(exec.task || '')}">${taskStr}</td>
|
<td title="${Utils.escapeHtml(exec.task || '')}">${taskStr}</td>
|
||||||
<td>${status}</td>
|
<td>${status}</td>
|
||||||
<td>${date}</td>
|
<td>${date}</td>
|
||||||
<td>${duration}</td>
|
<td>${duration}</td>
|
||||||
@@ -283,16 +283,6 @@ const SchedulesUI = {
|
|||||||
return `${minutes}m ${seconds}s`;
|
return `${minutes}m ${seconds}s`;
|
||||||
},
|
},
|
||||||
|
|
||||||
_escapeHtml(str) {
|
|
||||||
if (!str) return '';
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
},
|
|
||||||
|
|
||||||
_truncate(str, max) {
|
|
||||||
if (!str || str.length <= max) return str;
|
|
||||||
return str.slice(0, max) + '…';
|
|
||||||
},
|
|
||||||
|
|
||||||
cronToHuman(expression) {
|
cronToHuman(expression) {
|
||||||
if (!expression) return '—';
|
if (!expression) return '—';
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ const TasksUI = {
|
|||||||
return `
|
return `
|
||||||
<div class="task-card" data-task-id="${task.id}">
|
<div class="task-card" data-task-id="${task.id}">
|
||||||
<div class="task-card-header">
|
<div class="task-card-header">
|
||||||
<h4 class="task-card-name">${task.name}</h4>
|
<h4 class="task-card-name">${Utils.escapeHtml(task.name)}</h4>
|
||||||
<span class="badge ${categoryClass}">${categoryLabel}</span>
|
<span class="badge ${categoryClass}">${Utils.escapeHtml(categoryLabel)}</span>
|
||||||
</div>
|
</div>
|
||||||
${task.description ? `<p class="task-card-description">${task.description}</p>` : ''}
|
${task.description ? `<p class="task-card-description">${Utils.escapeHtml(task.description)}</p>` : ''}
|
||||||
<div class="task-card-footer">
|
<div class="task-card-footer">
|
||||||
<span class="task-card-date">
|
<span class="task-card-date">
|
||||||
<i data-lucide="calendar"></i>
|
<i data-lucide="calendar"></i>
|
||||||
@@ -117,7 +117,7 @@ const TasksUI = {
|
|||||||
<div class="task-card task-card--form" id="task-inline-form">
|
<div class="task-card task-card--form" id="task-inline-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="task-inline-name">${title}</label>
|
<label class="form-label" for="task-inline-name">${title}</label>
|
||||||
<input type="text" id="task-inline-name" class="input" placeholder="Ex: Code Review de PR" required autocomplete="off" value="${task.name || ''}">
|
<input type="text" id="task-inline-name" class="input" placeholder="Ex: Code Review de PR" required autocomplete="off" value="${Utils.escapeHtml(task.name || '')}">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="task-inline-category">Categoria</label>
|
<label class="form-label" for="task-inline-category">Categoria</label>
|
||||||
@@ -133,7 +133,7 @@ const TasksUI = {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="task-inline-description">Descrição</label>
|
<label class="form-label" for="task-inline-description">Descrição</label>
|
||||||
<textarea id="task-inline-description" class="textarea" rows="2" placeholder="Descreva o objetivo desta tarefa...">${task.description || ''}</textarea>
|
<textarea id="task-inline-description" class="textarea" rows="2" placeholder="Descreva o objetivo desta tarefa...">${Utils.escapeHtml(task.description || '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn btn--primary" id="btn-save-inline-task" type="button">${btnLabel}</button>
|
<button class="btn btn--primary" id="btn-save-inline-task" type="button">${btnLabel}</button>
|
||||||
@@ -228,7 +228,7 @@ const TasksUI = {
|
|||||||
const selectEl = document.getElementById('execute-agent-select');
|
const selectEl = document.getElementById('execute-agent-select');
|
||||||
if (selectEl) {
|
if (selectEl) {
|
||||||
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||||
activeAgents.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`).join('');
|
activeAgents.map((a) => `<option value="${a.id}">${Utils.escapeHtml(a.agent_name || a.name)}</option>`).join('');
|
||||||
selectEl.value = '';
|
selectEl.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const Terminal = {
|
|||||||
|
|
||||||
const html = lines.map((line) => {
|
const html = lines.map((line) => {
|
||||||
const typeClass = line.type && line.type !== 'default' ? ' ' + line.type : '';
|
const typeClass = line.type && line.type !== 'default' ? ' ' + line.type : '';
|
||||||
const escaped = Terminal._escapeHtml(line.content);
|
const escaped = Utils.escapeHtml(line.content);
|
||||||
const formatted = escaped.replace(/\n/g, '<br>');
|
const formatted = escaped.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
return `<div class="terminal-line${typeClass}">
|
return `<div class="terminal-line${typeClass}">
|
||||||
@@ -120,14 +120,6 @@ const Terminal = {
|
|||||||
if (Terminal.autoScroll) Terminal.scrollToBottom();
|
if (Terminal.autoScroll) Terminal.scrollToBottom();
|
||||||
},
|
},
|
||||||
|
|
||||||
_escapeHtml(text) {
|
|
||||||
return String(text)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.Terminal = Terminal;
|
window.Terminal = Terminal;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const WebhooksUI = {
|
|||||||
<article class="webhook-card">
|
<article class="webhook-card">
|
||||||
<div class="webhook-card-header">
|
<div class="webhook-card-header">
|
||||||
<div class="webhook-card-identity">
|
<div class="webhook-card-identity">
|
||||||
<span class="webhook-card-name">${WebhooksUI._escapeHtml(webhook.name)}</span>
|
<span class="webhook-card-name">${Utils.escapeHtml(webhook.name)}</span>
|
||||||
${typeBadge}
|
${typeBadge}
|
||||||
${statusBadge}
|
${statusBadge}
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +87,7 @@ const WebhooksUI = {
|
|||||||
<div class="webhook-card-body">
|
<div class="webhook-card-body">
|
||||||
<div class="webhook-card-target">
|
<div class="webhook-card-target">
|
||||||
<span class="webhook-card-label">Destino</span>
|
<span class="webhook-card-label">Destino</span>
|
||||||
<span class="webhook-card-value">${WebhooksUI._escapeHtml(targetName)}</span>
|
<span class="webhook-card-value">${Utils.escapeHtml(targetName)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="webhook-card-url">
|
<div class="webhook-card-url">
|
||||||
<span class="webhook-card-label">URL</span>
|
<span class="webhook-card-label">URL</span>
|
||||||
@@ -150,10 +150,10 @@ const WebhooksUI = {
|
|||||||
|
|
||||||
if (targetType === 'agent') {
|
if (targetType === 'agent') {
|
||||||
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||||
WebhooksUI.agents.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`).join('');
|
WebhooksUI.agents.map((a) => `<option value="${a.id}">${Utils.escapeHtml(a.agent_name || a.name)}</option>`).join('');
|
||||||
} else {
|
} else {
|
||||||
selectEl.innerHTML = '<option value="">Selecionar pipeline...</option>' +
|
selectEl.innerHTML = '<option value="">Selecionar pipeline...</option>' +
|
||||||
WebhooksUI.pipelines.map((p) => `<option value="${p.id}">${p.name}</option>`).join('');
|
WebhooksUI.pipelines.map((p) => `<option value="${p.id}">${Utils.escapeHtml(p.name)}</option>`).join('');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -243,15 +243,6 @@ const WebhooksUI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_escapeHtml(str) {
|
|
||||||
if (!str) return '';
|
|
||||||
return String(str)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.WebhooksUI = WebhooksUI;
|
window.WebhooksUI = WebhooksUI;
|
||||||
|
|||||||
32
public/js/utils.js
Normal file
32
public/js/utils.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const Utils = {
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDuration(ms) {
|
||||||
|
if (!ms || ms < 0) return '—';
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
const m = Math.floor(ms / 60000);
|
||||||
|
const s = Math.floor((ms % 60000) / 1000);
|
||||||
|
return `${m}m ${s}s`;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatCost(usd) {
|
||||||
|
if (!usd || usd === 0) return '$0.0000';
|
||||||
|
return `$${Number(usd).toFixed(4)}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
truncate(str, max = 80) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.length > max ? str.slice(0, max) + '…' : str;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Utils = Utils;
|
||||||
25
server.js
25
server.js
@@ -4,6 +4,7 @@ import { WebSocketServer } from 'ws';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import crypto from 'crypto';
|
||||||
import apiRouter, { setWsBroadcast, setWsBroadcastTo, hookRouter } from './src/routes/api.js';
|
import apiRouter, { setWsBroadcast, setWsBroadcastTo, hookRouter } from './src/routes/api.js';
|
||||||
import * as manager from './src/agents/manager.js';
|
import * as manager from './src/agents/manager.js';
|
||||||
import { setGlobalBroadcast } from './src/agents/manager.js';
|
import { setGlobalBroadcast } from './src/agents/manager.js';
|
||||||
@@ -14,6 +15,24 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const AUTH_TOKEN = process.env.AUTH_TOKEN || '';
|
const AUTH_TOKEN = process.env.AUTH_TOKEN || '';
|
||||||
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || '';
|
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || '';
|
||||||
|
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '';
|
||||||
|
|
||||||
|
function verifyWebhookSignature(req, res, next) {
|
||||||
|
if (!WEBHOOK_SECRET) return next();
|
||||||
|
const sig = req.headers['x-hub-signature-256'];
|
||||||
|
if (!sig) return res.status(401).json({ error: 'Assinatura ausente' });
|
||||||
|
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
|
||||||
|
hmac.update(req.rawBody || '');
|
||||||
|
const expected = 'sha256=' + hmac.digest('hex');
|
||||||
|
try {
|
||||||
|
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
|
||||||
|
return res.status(401).json({ error: 'Assinatura inválida' });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return res.status(401).json({ error: 'Assinatura inválida' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
@@ -39,8 +58,10 @@ if (AUTH_TOKEN) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json({
|
||||||
app.use('/hook', hookRouter);
|
verify: (req, res, buf) => { req.rawBody = buf; },
|
||||||
|
}));
|
||||||
|
app.use('/hook', verifyWebhookSignature, hookRouter);
|
||||||
app.use(express.static(join(__dirname, 'public')));
|
app.use(express.static(join(__dirname, 'public')));
|
||||||
app.use('/api', apiRouter);
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function createStore(filePath) {
|
|||||||
let mem = null;
|
let mem = null;
|
||||||
let dirty = false;
|
let dirty = false;
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
let maxSize = Infinity;
|
||||||
|
|
||||||
function boot() {
|
function boot() {
|
||||||
if (mem !== null) return;
|
if (mem !== null) return;
|
||||||
@@ -81,6 +82,9 @@ function createStore(filePath) {
|
|||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
mem.push(item);
|
mem.push(item);
|
||||||
|
if (maxSize !== Infinity && mem.length > maxSize) {
|
||||||
|
mem.splice(0, mem.length - maxSize);
|
||||||
|
}
|
||||||
touch();
|
touch();
|
||||||
return clone(item);
|
return clone(item);
|
||||||
},
|
},
|
||||||
@@ -118,6 +122,10 @@ function createStore(filePath) {
|
|||||||
dirty = false;
|
dirty = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setMaxSize(n) {
|
||||||
|
maxSize = n;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
allStores.push(store);
|
allStores.push(store);
|
||||||
@@ -185,5 +193,9 @@ export const tasksStore = createStore(`${DATA_DIR}/tasks.json`);
|
|||||||
export const pipelinesStore = createStore(`${DATA_DIR}/pipelines.json`);
|
export const pipelinesStore = createStore(`${DATA_DIR}/pipelines.json`);
|
||||||
export const schedulesStore = createStore(`${DATA_DIR}/schedules.json`);
|
export const schedulesStore = createStore(`${DATA_DIR}/schedules.json`);
|
||||||
export const executionsStore = createStore(`${DATA_DIR}/executions.json`);
|
export const executionsStore = createStore(`${DATA_DIR}/executions.json`);
|
||||||
|
executionsStore.setMaxSize(5000);
|
||||||
export const webhooksStore = createStore(`${DATA_DIR}/webhooks.json`);
|
export const webhooksStore = createStore(`${DATA_DIR}/webhooks.json`);
|
||||||
export const settingsStore = createSettingsStore(`${DATA_DIR}/settings.json`);
|
export const settingsStore = createSettingsStore(`${DATA_DIR}/settings.json`);
|
||||||
|
export const secretsStore = createStore(`${DATA_DIR}/secrets.json`);
|
||||||
|
export const notificationsStore = createStore(`${DATA_DIR}/notifications.json`);
|
||||||
|
export const agentVersionsStore = createStore(`${DATA_DIR}/agent_versions.json`);
|
||||||
|
|||||||
Reference in New Issue
Block a user