@fabioforest/openclaw 3.0.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.
Files changed (43) hide show
  1. package/README.md +88 -0
  2. package/bin/openclaw.js +152 -0
  3. package/lib/channels.js +84 -0
  4. package/lib/cli/debug.js +12 -0
  5. package/lib/cli/doctor.js +266 -0
  6. package/lib/cli/init.js +145 -0
  7. package/lib/cli/orchestrate.js +43 -0
  8. package/lib/cli/status.js +128 -0
  9. package/lib/cli/update.js +122 -0
  10. package/lib/config.js +68 -0
  11. package/lib/detect.js +49 -0
  12. package/lib/ops/audit.js +156 -0
  13. package/lib/ops/enroll.js +133 -0
  14. package/lib/ops/exec.js +140 -0
  15. package/lib/ops/healthcheck.js +167 -0
  16. package/lib/ops/policy.js +153 -0
  17. package/lib/ops/transfer.js +152 -0
  18. package/lib/ops/update-safe.js +173 -0
  19. package/lib/ops/vpn.js +131 -0
  20. package/lib/security.js +48 -0
  21. package/lib/setup/config_wizard.js +186 -0
  22. package/package.json +47 -0
  23. package/templates/.agent/agents/setup-specialist.md +24 -0
  24. package/templates/.agent/agents/sysadmin-proativo.md +31 -0
  25. package/templates/.agent/hooks/pre-tool-use.js +109 -0
  26. package/templates/.agent/rules/SECURITY.md +7 -0
  27. package/templates/.agent/skills/openclaw-installation-debugger/SKILL.md +37 -0
  28. package/templates/.agent/skills/openclaw-installation-debugger/scripts/debug.js +165 -0
  29. package/templates/.agent/skills/openclaw-ops/01-openclaw-vpn-wireguard/SKILL.md +20 -0
  30. package/templates/.agent/skills/openclaw-ops/02-openclaw-enroll-host/SKILL.md +14 -0
  31. package/templates/.agent/skills/openclaw-ops/03-openclaw-policy-baseline/SKILL.md +17 -0
  32. package/templates/.agent/skills/openclaw-ops/04-openclaw-remote-exec-runbooks/SKILL.md +13 -0
  33. package/templates/.agent/skills/openclaw-ops/05-openclaw-file-transfer-safe/SKILL.md +10 -0
  34. package/templates/.agent/skills/openclaw-ops/06-openclaw-audit-logging/SKILL.md +13 -0
  35. package/templates/.agent/skills/openclaw-ops/07-openclaw-safe-update/SKILL.md +9 -0
  36. package/templates/.agent/skills/openclaw-ops/08-openclaw-healthchecks/SKILL.md +10 -0
  37. package/templates/.agent/skills/universal-setup/SKILL.md +26 -0
  38. package/templates/.agent/workflows/doctor.md +20 -0
  39. package/templates/.agent/workflows/healthcheck.md +22 -0
  40. package/templates/.agent/workflows/healthcheck.runbook.md +9 -0
  41. package/templates/.agent/workflows/restart.md +20 -0
  42. package/templates/.agent/workflows/restart.runbook.md +8 -0
  43. package/templates/.agent/workflows/setup.md +18 -0
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Script operacional: Audit Logging
5
+ *
6
+ * Auditoria estruturada (JSON) para todos os eventos do OpenClaw.
7
+ * Inclui request_id, redaction de segredos e rotação de logs.
8
+ *
9
+ * Referência: skills/openclaw-ops/06-openclaw-audit-logging/SKILL.md
10
+ */
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const { mask } = require("../security");
15
+
16
+ // Campos que devem ter seus valores mascarados no log
17
+ const SENSITIVE_FIELDS = ["token", "password", "secret", "privateKey", "apiKey", "auth_token"];
18
+
19
+ /**
20
+ * Mascara campos sensíveis recursivamente em um objeto.
21
+ * @param {object} obj — objeto a ser processado
22
+ * @returns {object} cópia com valores sensíveis mascarados
23
+ */
24
+ function redactSensitive(obj) {
25
+ if (!obj || typeof obj !== "object") return obj;
26
+
27
+ const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
28
+
29
+ for (const [key, value] of Object.entries(redacted)) {
30
+ if (typeof value === "object" && value !== null) {
31
+ redacted[key] = redactSensitive(value);
32
+ } else if (typeof value === "string" && SENSITIVE_FIELDS.includes(key)) {
33
+ redacted[key] = mask(value);
34
+ }
35
+ }
36
+
37
+ return redacted;
38
+ }
39
+
40
+ /**
41
+ * Cria uma entrada de log de auditoria.
42
+ * @param {object} params
43
+ * @param {string} params.event — tipo do evento (ex: "exec.started")
44
+ * @param {string} params.requestId — ID da requisição
45
+ * @param {string} [params.operator] — quem iniciou a ação
46
+ * @param {string} [params.hostId] — host onde ocorreu
47
+ * @param {object} [params.details] — detalhes adicionais
48
+ * @returns {object} entrada de auditoria formatada
49
+ */
50
+ function createAuditEntry({ event, requestId, operator, hostId, details = {} }) {
51
+ return {
52
+ timestamp: new Date().toISOString(),
53
+ event,
54
+ requestId,
55
+ operator: operator || "system",
56
+ hostId: hostId || "local",
57
+ details: redactSensitive(details),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Escreve uma entrada de auditoria no arquivo de log.
63
+ * @param {string} logDir — diretório de logs
64
+ * @param {object} entry — entrada de auditoria
65
+ */
66
+ function writeAuditLog(logDir, entry) {
67
+ if (!fs.existsSync(logDir)) {
68
+ fs.mkdirSync(logDir, { recursive: true });
69
+ }
70
+
71
+ // Arquivo de log diário
72
+ const date = new Date().toISOString().split("T")[0];
73
+ const logFile = path.join(logDir, `audit-${date}.jsonl`);
74
+
75
+ // Append em formato JSONL (uma linha JSON por entrada)
76
+ fs.appendFileSync(logFile, JSON.stringify(entry) + "\n", "utf8");
77
+ }
78
+
79
+ /**
80
+ * Lê entradas de auditoria de um período.
81
+ * @param {string} logDir — diretório de logs
82
+ * @param {object} [filter] — filtros opcionais
83
+ * @param {string} [filter.date] — data específica (YYYY-MM-DD)
84
+ * @param {string} [filter.event] — filtrar por tipo de evento
85
+ * @param {string} [filter.requestId] — filtrar por request_id
86
+ * @param {number} [filter.limit=100] — limite de entradas
87
+ * @returns {object[]} entradas de auditoria
88
+ */
89
+ function readAuditLogs(logDir, filter = {}) {
90
+ if (!fs.existsSync(logDir)) return [];
91
+
92
+ const date = filter.date || new Date().toISOString().split("T")[0];
93
+ const logFile = path.join(logDir, `audit-${date}.jsonl`);
94
+
95
+ if (!fs.existsSync(logFile)) return [];
96
+
97
+ const lines = fs.readFileSync(logFile, "utf8")
98
+ .split("\n")
99
+ .filter((line) => line.trim());
100
+
101
+ let entries = lines.map((line) => {
102
+ try {
103
+ return JSON.parse(line);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }).filter(Boolean);
108
+
109
+ // Aplicar filtros
110
+ if (filter.event) {
111
+ entries = entries.filter((e) => e.event === filter.event);
112
+ }
113
+ if (filter.requestId) {
114
+ entries = entries.filter((e) => e.requestId === filter.requestId);
115
+ }
116
+
117
+ // Limite
118
+ const limit = filter.limit || 100;
119
+ return entries.slice(-limit);
120
+ }
121
+
122
+ /**
123
+ * Rotaciona logs mais antigos que o período de retenção.
124
+ * @param {string} logDir — diretório de logs
125
+ * @param {number} [retentionDays=30] — dias de retenção
126
+ * @returns {number} quantidade de arquivos removidos
127
+ */
128
+ function rotateLogs(logDir, retentionDays = 30) {
129
+ if (!fs.existsSync(logDir)) return 0;
130
+
131
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
132
+ let removed = 0;
133
+
134
+ const files = fs.readdirSync(logDir).filter((f) => f.startsWith("audit-") && f.endsWith(".jsonl"));
135
+
136
+ for (const file of files) {
137
+ const filePath = path.join(logDir, file);
138
+ const stats = fs.statSync(filePath);
139
+
140
+ if (stats.mtimeMs < cutoff) {
141
+ fs.unlinkSync(filePath);
142
+ removed++;
143
+ }
144
+ }
145
+
146
+ return removed;
147
+ }
148
+
149
+ module.exports = {
150
+ redactSensitive,
151
+ createAuditEntry,
152
+ writeAuditLog,
153
+ readAuditLogs,
154
+ rotateLogs,
155
+ SENSITIVE_FIELDS,
156
+ };
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Script operacional: Enroll Host
5
+ *
6
+ * Onboarding seguro de host na malha WireGuard + OpenClaw
7
+ * com aprovação humana, identidade e revogação rápida.
8
+ *
9
+ * Referência: skills/openclaw-ops/02-openclaw-enroll-host/SKILL.md
10
+ */
11
+
12
+ const crypto = require("crypto");
13
+ const os = require("os");
14
+ const path = require("path");
15
+ const { readJsonSafe, writeJsonSafe } = require("../config");
16
+
17
+ /**
18
+ * Gera um identificador único para o host.
19
+ * @returns {{ hostId: string, fingerprint: string, hostname: string }}
20
+ */
21
+ function generateHostIdentity() {
22
+ const hostId = crypto.randomUUID();
23
+ const hostname = os.hostname();
24
+
25
+ // Fingerprint baseado em hostname + timestamp + random
26
+ const raw = `${hostname}-${Date.now()}-${crypto.randomBytes(8).toString("hex")}`;
27
+ const fingerprint = crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
28
+
29
+ return { hostId, fingerprint, hostname };
30
+ }
31
+
32
+ /**
33
+ * Registra um host no registro de hosts pendentes.
34
+ * @param {string} registryPath — caminho do arquivo de registro
35
+ * @param {object} hostInfo — informações do host (hostId, fingerprint, hostname)
36
+ * @returns {{ success: boolean, host: object }}
37
+ */
38
+ function registerHost(registryPath, hostInfo) {
39
+ const registry = readJsonSafe(registryPath) || { hosts: [] };
40
+
41
+ const entry = {
42
+ ...hostInfo,
43
+ status: "pending",
44
+ registeredAt: new Date().toISOString(),
45
+ approvedAt: null,
46
+ approvedBy: null,
47
+ };
48
+
49
+ registry.hosts.push(entry);
50
+ writeJsonSafe(registryPath, registry);
51
+
52
+ return { success: true, host: entry };
53
+ }
54
+
55
+ /**
56
+ * Aprova um host pendente.
57
+ * @param {string} registryPath — caminho do arquivo de registro
58
+ * @param {string} hostId — ID do host a aprovar
59
+ * @param {string} approvedBy — quem aprovou
60
+ * @returns {{ success: boolean, host?: object, error?: string }}
61
+ */
62
+ function approveHost(registryPath, hostId, approvedBy) {
63
+ const registry = readJsonSafe(registryPath);
64
+ if (!registry || !registry.hosts) {
65
+ return { success: false, error: "Registro de hosts não encontrado" };
66
+ }
67
+
68
+ const host = registry.hosts.find((h) => h.hostId === hostId);
69
+ if (!host) {
70
+ return { success: false, error: `Host ${hostId} não encontrado` };
71
+ }
72
+
73
+ if (host.status === "approved") {
74
+ return { success: false, error: `Host ${hostId} já está aprovado` };
75
+ }
76
+
77
+ host.status = "approved";
78
+ host.approvedAt = new Date().toISOString();
79
+ host.approvedBy = approvedBy;
80
+
81
+ writeJsonSafe(registryPath, registry);
82
+ return { success: true, host };
83
+ }
84
+
85
+ /**
86
+ * Revoga um host (remove acesso).
87
+ * @param {string} registryPath — caminho do arquivo de registro
88
+ * @param {string} hostId — ID do host a revogar
89
+ * @param {string} revokedBy — quem revogou
90
+ * @returns {{ success: boolean, host?: object, error?: string }}
91
+ */
92
+ function revokeHost(registryPath, hostId, revokedBy) {
93
+ const registry = readJsonSafe(registryPath);
94
+ if (!registry || !registry.hosts) {
95
+ return { success: false, error: "Registro de hosts não encontrado" };
96
+ }
97
+
98
+ const host = registry.hosts.find((h) => h.hostId === hostId);
99
+ if (!host) {
100
+ return { success: false, error: `Host ${hostId} não encontrado` };
101
+ }
102
+
103
+ host.status = "revoked";
104
+ host.revokedAt = new Date().toISOString();
105
+ host.revokedBy = revokedBy;
106
+
107
+ writeJsonSafe(registryPath, registry);
108
+ return { success: true, host };
109
+ }
110
+
111
+ /**
112
+ * Lista hosts filtrados por status.
113
+ * @param {string} registryPath — caminho do arquivo de registro
114
+ * @param {string} [statusFilter] — filtrar por status (pending, approved, revoked)
115
+ * @returns {object[]} lista de hosts
116
+ */
117
+ function listHosts(registryPath, statusFilter) {
118
+ const registry = readJsonSafe(registryPath);
119
+ if (!registry || !registry.hosts) return [];
120
+
121
+ if (statusFilter) {
122
+ return registry.hosts.filter((h) => h.status === statusFilter);
123
+ }
124
+ return registry.hosts;
125
+ }
126
+
127
+ module.exports = {
128
+ generateHostIdentity,
129
+ registerHost,
130
+ approveHost,
131
+ revokeHost,
132
+ listHosts,
133
+ };
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Script operacional: Remote Exec (Runbooks)
5
+ *
6
+ * Estrutura execução remota via VPN usando runbooks nomeados,
7
+ * idempotentes, com timeout, cancel e trilha auditável.
8
+ *
9
+ * Referência: skills/openclaw-ops/04-openclaw-remote-exec-runbooks/SKILL.md
10
+ */
11
+
12
+ const { execSync, spawn } = require("child_process");
13
+ const crypto = require("crypto");
14
+
15
+ /**
16
+ * Gera um request_id único para rastrear a execução.
17
+ * @returns {string} request_id no formato "req-<uuid>"
18
+ */
19
+ function generateRequestId() {
20
+ return `req-${crypto.randomUUID()}`;
21
+ }
22
+
23
+ /**
24
+ * Valida as entradas de um runbook antes da execução.
25
+ * @param {object} runbook — definição do runbook
26
+ * @param {string} runbook.name — nome do runbook
27
+ * @param {string} runbook.command — comando a executar
28
+ * @param {string[]} [runbook.allowedArgs] — args permitidos
29
+ * @param {object} [inputs] — inputs do usuário
30
+ * @returns {{ valid: boolean, errors: string[] }}
31
+ */
32
+ function validateInputs(runbook, inputs = {}) {
33
+ const errors = [];
34
+
35
+ if (!runbook.name) errors.push("Nome do runbook é obrigatório");
36
+ if (!runbook.command) errors.push("Comando do runbook é obrigatório");
37
+
38
+ // Validar que inputs não contêm injection
39
+ for (const [key, value] of Object.entries(inputs)) {
40
+ if (typeof value === "string" && /[;&|`$()]/.test(value)) {
41
+ errors.push(`Input '${key}' contém caracteres proibidos: ${value}`);
42
+ }
43
+ }
44
+
45
+ return { valid: errors.length === 0, errors };
46
+ }
47
+
48
+ /**
49
+ * Executa um runbook de forma segura com timeout e captura de saída.
50
+ * @param {object} params
51
+ * @param {string} params.command — comando a executar
52
+ * @param {number} [params.timeoutMs=30000] — timeout em ms
53
+ * @param {string} [params.cwd] — diretório de trabalho
54
+ * @returns {{ requestId: string, exitCode: number, stdout: string, stderr: string, timedOut: boolean, duration: number }}
55
+ */
56
+ function executeRunbook({ command, timeoutMs = 30000, cwd }) {
57
+ const requestId = generateRequestId();
58
+ const startTime = Date.now();
59
+
60
+ try {
61
+ const stdout = execSync(command, {
62
+ encoding: "utf8",
63
+ timeout: timeoutMs,
64
+ cwd: cwd || undefined,
65
+ maxBuffer: 1024 * 1024, // 1MB
66
+ });
67
+
68
+ return {
69
+ requestId,
70
+ exitCode: 0,
71
+ stdout: stdout.trim(),
72
+ stderr: "",
73
+ timedOut: false,
74
+ duration: Date.now() - startTime,
75
+ };
76
+ } catch (err) {
77
+ return {
78
+ requestId,
79
+ exitCode: err.status || 1,
80
+ stdout: (err.stdout || "").trim(),
81
+ stderr: (err.stderr || err.message || "").trim(),
82
+ timedOut: err.killed || false,
83
+ duration: Date.now() - startTime,
84
+ };
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Executa um runbook em background com possibilidade de cancelamento.
90
+ * @param {object} params
91
+ * @param {string} params.command — comando a executar
92
+ * @param {number} [params.timeoutMs=30000] — timeout em ms
93
+ * @returns {{ requestId: string, process: object, cancel: function }}
94
+ */
95
+ function executeAsync({ command, timeoutMs = 30000 }) {
96
+ const requestId = generateRequestId();
97
+ const parts = command.split(" ");
98
+ const child = spawn(parts[0], parts.slice(1), {
99
+ timeout: timeoutMs,
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ });
102
+
103
+ const cancel = () => {
104
+ if (!child.killed) {
105
+ child.kill("SIGTERM");
106
+ // Force kill após 5s se não responder
107
+ setTimeout(() => {
108
+ if (!child.killed) child.kill("SIGKILL");
109
+ }, 5000);
110
+ }
111
+ };
112
+
113
+ return { requestId, process: child, cancel };
114
+ }
115
+
116
+ /**
117
+ * Formata o resultado de uma execução para log.
118
+ * @param {object} result — resultado da execução
119
+ * @returns {object} objeto formatado para auditoria
120
+ */
121
+ function formatForAudit(result) {
122
+ return {
123
+ requestId: result.requestId,
124
+ exitCode: result.exitCode,
125
+ timedOut: result.timedOut,
126
+ duration: `${result.duration}ms`,
127
+ timestamp: new Date().toISOString(),
128
+ // Trunca saída para auditoria (max 500 chars)
129
+ stdoutPreview: result.stdout.slice(0, 500),
130
+ stderrPreview: result.stderr.slice(0, 500),
131
+ };
132
+ }
133
+
134
+ module.exports = {
135
+ generateRequestId,
136
+ validateInputs,
137
+ executeRunbook,
138
+ executeAsync,
139
+ formatForAudit,
140
+ };
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Script operacional: Healthchecks
5
+ *
6
+ * Heartbeat via VPN, alertas e autocura com limites.
7
+ * Circuit breaker para bloquear exec quando instável.
8
+ *
9
+ * Referência: skills/openclaw-ops/08-openclaw-healthchecks/SKILL.md
10
+ */
11
+
12
+ const { portInUse } = require("../security");
13
+
14
+ /**
15
+ * Estado do circuit breaker.
16
+ * @typedef {'closed'|'open'|'half-open'} CircuitState
17
+ */
18
+
19
+ /**
20
+ * Cria uma instância de circuit breaker.
21
+ * @param {object} [options]
22
+ * @param {number} [options.failureThreshold=2] — falhas consecutivas para abrir
23
+ * @param {number} [options.resetTimeoutMs=60000] — tempo para tentar half-open (ms)
24
+ * @returns {object} circuit breaker com métodos record, canExecute, getState, reset
25
+ */
26
+ function createCircuitBreaker({ failureThreshold = 2, resetTimeoutMs = 60000 } = {}) {
27
+ let state = "closed";
28
+ let failures = 0;
29
+ let lastFailureTime = null;
30
+
31
+ return {
32
+ /**
33
+ * Registra resultado de uma operação.
34
+ * @param {boolean} success — se a operação teve sucesso
35
+ * @returns {CircuitState} estado atual após o registro
36
+ */
37
+ record(success) {
38
+ if (success) {
39
+ failures = 0;
40
+ state = "closed";
41
+ return state;
42
+ }
43
+
44
+ failures++;
45
+ lastFailureTime = Date.now();
46
+
47
+ if (failures >= failureThreshold) {
48
+ state = "open";
49
+ }
50
+
51
+ return state;
52
+ },
53
+
54
+ /**
55
+ * Verifica se é seguro executar uma operação.
56
+ * @returns {{ allowed: boolean, state: CircuitState, reason?: string }}
57
+ */
58
+ canExecute() {
59
+ if (state === "closed") {
60
+ return { allowed: true, state };
61
+ }
62
+
63
+ if (state === "open") {
64
+ // Verificar se já passou o timeout para half-open
65
+ const elapsed = Date.now() - (lastFailureTime || 0);
66
+ if (elapsed >= resetTimeoutMs) {
67
+ state = "half-open";
68
+ return { allowed: true, state, reason: "Testando recuperação (half-open)" };
69
+ }
70
+ return {
71
+ allowed: false,
72
+ state,
73
+ reason: `Circuit breaker aberto — ${failures} falhas consecutivas. Aguardando ${Math.ceil((resetTimeoutMs - elapsed) / 1000)}s`,
74
+ };
75
+ }
76
+
77
+ // half-open: permite uma tentativa
78
+ return { allowed: true, state, reason: "Half-open: tentativa de recuperação" };
79
+ },
80
+
81
+ /** Retorna o estado atual. */
82
+ getState() {
83
+ return { state, failures, lastFailureTime };
84
+ },
85
+
86
+ /** Reseta o circuit breaker. */
87
+ reset() {
88
+ state = "closed";
89
+ failures = 0;
90
+ lastFailureTime = null;
91
+ },
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Executa um heartbeat verificando a saúde básica do sistema.
97
+ * @param {object} [options]
98
+ * @param {number} [options.port=18789] — porta a verificar
99
+ * @param {string} [options.host=127.0.0.1] — host a verificar
100
+ * @returns {Promise<{ healthy: boolean, checks: object[] }>}
101
+ */
102
+ async function heartbeat({ port = 18789, host = "127.0.0.1" } = {}) {
103
+ const checks = [];
104
+
105
+ // Check 1: Porta do serviço
106
+ try {
107
+ const inUse = await portInUse(port, host);
108
+ checks.push({
109
+ name: `port:${port}`,
110
+ status: inUse ? "ok" : "fail",
111
+ message: inUse ? `Serviço respondendo em ${host}:${port}` : `Porta ${port} livre — serviço pode estar parado`,
112
+ });
113
+ } catch (err) {
114
+ checks.push({
115
+ name: `port:${port}`,
116
+ status: "fail",
117
+ message: `Erro ao verificar porta: ${err.message}`,
118
+ });
119
+ }
120
+
121
+ const healthy = checks.every((c) => c.status === "ok");
122
+ return { healthy, checks, timestamp: new Date().toISOString() };
123
+ }
124
+
125
+ /**
126
+ * Tenta auto-restart com limites de tentativas.
127
+ * @param {object} params
128
+ * @param {function} params.restartFn — função que executa o restart
129
+ * @param {function} params.healthFn — função que verifica saúde
130
+ * @param {number} [params.maxRetries=3] — máximo de tentativas
131
+ * @param {number} [params.delayMs=5000] — delay entre tentativas (ms)
132
+ * @returns {Promise<{ success: boolean, attempts: number, error?: string }>}
133
+ */
134
+ async function autoRestart({ restartFn, healthFn, maxRetries = 3, delayMs = 5000 }) {
135
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
136
+ try {
137
+ await restartFn();
138
+
139
+ // Aguardar antes de verificar
140
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
141
+
142
+ const healthy = await healthFn();
143
+ if (healthy) {
144
+ return { success: true, attempts: attempt };
145
+ }
146
+ } catch (err) {
147
+ if (attempt === maxRetries) {
148
+ return {
149
+ success: false,
150
+ attempts: attempt,
151
+ error: `Falha após ${maxRetries} tentativas: ${err.message}`,
152
+ };
153
+ }
154
+ }
155
+
156
+ // Delay incremental entre tentativas
157
+ await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
158
+ }
159
+
160
+ return { success: false, attempts: maxRetries, error: "Excedido limite de tentativas" };
161
+ }
162
+
163
+ module.exports = {
164
+ createCircuitBreaker,
165
+ heartbeat,
166
+ autoRestart,
167
+ };