hobots 1.5.1 → 1.6.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.
package/dist/index.js CHANGED
@@ -1,20 +1,46 @@
1
1
  // src/config.ts
2
2
  var DEFAULT_BASE_URL = "https://api2.hobots.app";
3
+ var SECRET_PATTERN = /^(hb_)?[a-f0-9]{64}$/i;
3
4
  function envBaseUrl() {
4
5
  if (typeof process === "undefined" || !process.env) return void 0;
5
6
  return process.env.HOBOTS_API_URL?.trim() || void 0;
6
7
  }
8
+ function isLoopbackHost(hostname2) {
9
+ return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
10
+ }
11
+ var warnedInsecureBaseUrl = false;
12
+ function validateBaseUrl(raw, prefix) {
13
+ let url;
14
+ try {
15
+ url = new URL(raw);
16
+ } catch {
17
+ throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
18
+ }
19
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
20
+ throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
21
+ }
22
+ if (url.username || url.password) {
23
+ throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
24
+ }
25
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
26
+ warnedInsecureBaseUrl = true;
27
+ console.warn(
28
+ `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
29
+ );
30
+ }
31
+ return raw.replace(/\/+$/, "");
32
+ }
7
33
  function resolveConfig(options, prefix = "[hobots]") {
8
34
  const key = options.clientSecret?.trim();
9
35
  if (!key) {
10
36
  throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`);
11
37
  }
12
- const baseUrl = (envBaseUrl() ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
13
- try {
14
- new URL(baseUrl);
15
- } catch {
16
- throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida: "${baseUrl}" \u2014 esperado ex.: http://localhost:3001`);
38
+ if (!SECRET_PATTERN.test(key)) {
39
+ throw new Error(
40
+ `${prefix} clientSecret com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`
41
+ );
17
42
  }
43
+ const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
18
44
  return {
19
45
  key,
20
46
  baseUrl,
@@ -56,9 +82,28 @@ function parseStacktrace(stack) {
56
82
  return frames;
57
83
  }
58
84
 
85
+ // src/scrub.ts
86
+ var SECRET_PATTERNS = [
87
+ // client secret da Hobots
88
+ [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
89
+ // tokens bearer em headers/mensagens
90
+ [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
91
+ // chaves no formato sk-… (OpenAI e afins)
92
+ [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
93
+ // credenciais embutidas em URL (https://user:pass@host)
94
+ [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
95
+ ];
96
+ function scrubSecrets(text) {
97
+ let out = text;
98
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
99
+ out = out.replace(pattern, replacement);
100
+ }
101
+ return out;
102
+ }
103
+
59
104
  // src/utils.ts
60
105
  var SDK_NAME = "hobots";
61
- var SDK_VERSION = true ? "1.5.0" : "0.0.0-dev";
106
+ var SDK_VERSION = true ? "1.6.0" : "0.0.0-dev";
62
107
  function isNode() {
63
108
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
64
109
  }
@@ -67,6 +112,12 @@ function uuid4() {
67
112
  if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
68
113
  return cryptoRef.randomUUID().replace(/-/g, "");
69
114
  }
115
+ if (cryptoRef && typeof cryptoRef.getRandomValues === "function") {
116
+ const bytes = cryptoRef.getRandomValues(new Uint8Array(16));
117
+ bytes[6] = bytes[6] & 15 | 64;
118
+ bytes[8] = bytes[8] & 63 | 128;
119
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
120
+ }
70
121
  let out = "";
71
122
  for (let i = 0; i < 32; i++) {
72
123
  out += Math.floor(Math.random() * 16).toString(16);
@@ -85,7 +136,42 @@ function sleep(ms) {
85
136
  unrefTimer(timer);
86
137
  });
87
138
  }
88
- var PREFIX = "[hobots-monitor]";
139
+ function jitter(baseMs) {
140
+ return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
141
+ }
142
+ var MAX_RETRY_AFTER_MS = 6e4;
143
+ function retryAfterMs(response) {
144
+ const header = response.headers.get("retry-after");
145
+ if (!header) return void 0;
146
+ const seconds = Number(header);
147
+ if (Number.isFinite(seconds)) {
148
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, seconds * 1e3));
149
+ }
150
+ const date = Date.parse(header);
151
+ if (Number.isNaN(date)) return void 0;
152
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, date - Date.now()));
153
+ }
154
+ function withTimeoutSignal(timeoutMs, external) {
155
+ const controller = new AbortController();
156
+ const onAbort = () => controller.abort(external?.reason);
157
+ if (external?.aborted) {
158
+ onAbort();
159
+ } else {
160
+ external?.addEventListener("abort", onAbort, { once: true });
161
+ }
162
+ const timer = setTimeout(() => {
163
+ controller.abort(Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms`), { name: "TimeoutError" }));
164
+ }, timeoutMs);
165
+ unrefTimer(timer);
166
+ return {
167
+ signal: controller.signal,
168
+ cleanup: () => {
169
+ clearTimeout(timer);
170
+ external?.removeEventListener("abort", onAbort);
171
+ }
172
+ };
173
+ }
174
+ var PREFIX = "[hobots]";
89
175
  function makeLogger(enabled, prefix = PREFIX) {
90
176
  return {
91
177
  debug: (...args) => enabled && console.debug(colorize(prefix, "gray"), ...args),
@@ -126,11 +212,11 @@ function eventFromException(error, level = "error") {
126
212
  let frames = [];
127
213
  if (error instanceof Error) {
128
214
  type = error.name || "Error";
129
- value = error.message;
215
+ value = scrubSecrets(error.message);
130
216
  frames = parseStacktrace(error.stack);
131
217
  } else {
132
218
  type = "NonError";
133
- value = safeString(error);
219
+ value = scrubSecrets(safeString(error));
134
220
  }
135
221
  return {
136
222
  event_id: uuid4(),
@@ -148,12 +234,13 @@ function eventFromMessage(message, level = "info") {
148
234
  event_id: uuid4(),
149
235
  timestamp: nowISO(),
150
236
  level,
151
- message
237
+ message: scrubSecrets(message)
152
238
  };
153
239
  }
154
240
 
155
241
  // src/transport.ts
156
242
  var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
243
+ var REQUEST_TIMEOUT_MS = 1e4;
157
244
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
158
245
  var DEFAULT_MAX_BATCH_SIZE = 10;
159
246
  var DEFAULT_MAX_QUEUE_SIZE = 200;
@@ -220,11 +307,13 @@ var Transport = class {
220
307
  }
221
308
  async sendWithRetry(envelope) {
222
309
  for (let attempt = 0; ; attempt++) {
310
+ let serverDelay;
223
311
  try {
224
312
  const response = await fetch(this.endpoint, {
225
313
  method: "POST",
226
314
  headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
227
- body: JSON.stringify(envelope)
315
+ body: JSON.stringify(envelope),
316
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
228
317
  });
229
318
  if (response.ok) {
230
319
  this.logger.debug(`envelope enviado (${envelope.items.length} itens)`);
@@ -234,13 +323,15 @@ var Transport = class {
234
323
  this.logger.error(`envelope rejeitado (HTTP ${response.status}), descartado`);
235
324
  return;
236
325
  }
326
+ if (response.status === 429) serverDelay = retryAfterMs(response);
237
327
  throw new Error(`HTTP ${response.status}`);
238
328
  } catch (error) {
239
- const delay = RETRY_DELAYS_MS[attempt];
240
- if (delay === void 0) {
329
+ const base = RETRY_DELAYS_MS[attempt];
330
+ if (base === void 0) {
241
331
  this.logger.error("envelope descartado ap\xF3s retries:", error);
242
332
  return;
243
333
  }
334
+ const delay = serverDelay ?? jitter(base);
244
335
  this.logger.warn(`falha no envio (${String(error)}), retry em ${delay}ms`);
245
336
  await sleep(delay);
246
337
  }
@@ -270,22 +361,38 @@ function runtimeContexts() {
270
361
  return { runtime: { name: "browser" } };
271
362
  }
272
363
  var Client = class {
273
- options;
274
- config;
275
364
  logger;
365
+ // campos privados + getters de protótipo: o clientSecret não aparece em
366
+ // JSON.stringify/serialização enumerável do Client (ex.: um objeto capturado
367
+ // que referencie o client), mas client.options/config seguem funcionando
368
+ #options;
369
+ #config;
276
370
  transport;
277
371
  constructor(options) {
278
- this.options = options;
372
+ this.#options = options;
279
373
  this.logger = makeLogger(!!options.debug);
280
- this.config = resolveConfig(options);
374
+ this.#config = resolveConfig(options);
281
375
  this.transport = new Transport({
282
- endpoint: this.config.ingestEndpoint,
283
- key: this.config.key,
376
+ endpoint: this.#config.ingestEndpoint,
377
+ key: this.#config.key,
284
378
  logger: this.logger,
285
379
  flushIntervalMs: options.flushIntervalMs,
286
380
  maxBatchSize: options.maxBatchSize
287
381
  });
288
382
  }
383
+ get options() {
384
+ return this.#options;
385
+ }
386
+ get config() {
387
+ return this.#config;
388
+ }
389
+ /** Serialização explícita nunca inclui o secret. */
390
+ toJSON() {
391
+ return { sdk: SDK_NAME, version: SDK_VERSION, baseUrl: this.#config.baseUrl };
392
+ }
393
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
394
+ return `Client { baseUrl: '${this.#config.baseUrl}' }`;
395
+ }
289
396
  captureException(error, hint, scope2) {
290
397
  return this.captureEvent(eventFromException(error, hint?.level ?? "error"), hint, scope2);
291
398
  }
@@ -704,6 +811,8 @@ async function withMonitorImpl(client2, captureError, monitorSlug, fn, options)
704
811
 
705
812
  // src/manager/api.ts
706
813
  var RETRY_DELAYS_MS2 = [1e3, 4e3, 16e3];
814
+ var POLL_TIMEOUT_MS = 3e4;
815
+ var UPDATE_TIMEOUT_MS = 1e4;
707
816
  var ManagerApiError = class extends Error {
708
817
  constructor(message, status) {
709
818
  super(message);
@@ -728,16 +837,39 @@ var ManagerApi = class {
728
837
  }
729
838
  /** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
730
839
  async poll(request, signal) {
731
- const response = await fetch(`${this.baseUrl}/poll`, {
732
- method: "POST",
733
- headers: this.headers(),
734
- body: JSON.stringify(request),
735
- signal
736
- });
840
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
841
+ let response;
842
+ try {
843
+ response = await fetch(`${this.baseUrl}/poll`, {
844
+ method: "POST",
845
+ headers: this.headers(),
846
+ body: JSON.stringify(request),
847
+ signal: timeoutSignal
848
+ });
849
+ } finally {
850
+ cleanup();
851
+ }
737
852
  if (!response.ok) {
738
853
  throw new ManagerApiError(`poll falhou (HTTP ${response.status})`, response.status);
739
854
  }
740
- return await response.json();
855
+ return this.parsePollResponse(await response.json(), request.max ?? 1);
856
+ }
857
+ /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
858
+ parsePollResponse(body, max) {
859
+ const runs = body?.runs;
860
+ if (!Array.isArray(runs)) {
861
+ throw new ManagerApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
862
+ }
863
+ const valid = [];
864
+ for (const run of runs.slice(0, Math.max(0, max))) {
865
+ if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
866
+ const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
867
+ valid.push({ ...run, id: run.id, task: run.task, params });
868
+ }
869
+ if (valid.length < runs.length) {
870
+ this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
871
+ }
872
+ return { runs: valid };
741
873
  }
742
874
  /**
743
875
  * PATCH de status/logs com retry em falha de rede/5xx/429.
@@ -746,25 +878,29 @@ var ManagerApi = class {
746
878
  async updateRun(runId, update) {
747
879
  for (let attempt = 0; ; attempt++) {
748
880
  let retriable;
881
+ let serverDelay;
749
882
  try {
750
883
  const response = await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
751
884
  method: "PATCH",
752
885
  headers: this.headers(),
753
- body: JSON.stringify(update)
886
+ body: JSON.stringify(update),
887
+ signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
754
888
  });
755
889
  if (response.ok) return;
756
890
  if (response.status >= 400 && response.status < 500 && response.status !== 429) {
757
891
  throw new ManagerApiError(`update rejeitado (HTTP ${response.status})`, response.status);
758
892
  }
893
+ if (response.status === 429) serverDelay = retryAfterMs(response);
759
894
  retriable = new Error(`HTTP ${response.status}`);
760
895
  } catch (error) {
761
896
  if (error instanceof ManagerApiError) throw error;
762
897
  retriable = error;
763
898
  }
764
- const delay = RETRY_DELAYS_MS2[attempt];
765
- if (delay === void 0) {
899
+ const base = RETRY_DELAYS_MS2[attempt];
900
+ if (base === void 0) {
766
901
  throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
767
902
  }
903
+ const delay = serverDelay ?? jitter(base);
768
904
  this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
769
905
  await sleep(delay);
770
906
  }
@@ -888,13 +1024,14 @@ var RunReporter = class {
888
1024
  }
889
1025
  log(level, message) {
890
1026
  if (this.dead) return;
891
- printRunLog(this.context.task, level, message);
1027
+ const scrubbed = scrubSecrets(message);
1028
+ printRunLog(this.context.task, level, scrubbed);
892
1029
  if (this.loggedCount >= MAX_LOGS_PER_RUN) {
893
1030
  this.droppedCount++;
894
1031
  return;
895
1032
  }
896
1033
  this.loggedCount++;
897
- const truncated = message.length > MAX_LOG_MESSAGE_CHARS ? `${message.slice(0, MAX_LOG_MESSAGE_CHARS)}\u2026 [truncado]` : message;
1034
+ const truncated = scrubbed.length > MAX_LOG_MESSAGE_CHARS ? `${scrubbed.slice(0, MAX_LOG_MESSAGE_CHARS)}\u2026 [truncado]` : scrubbed;
898
1035
  this.buffer.push({ ts: nowISO(), level, message: truncated });
899
1036
  if (this.buffer.length >= FLUSH_AT_BUFFERED) {
900
1037
  this.flushLogs();
@@ -1078,8 +1215,8 @@ var ManagerAgent = class {
1078
1215
  constructor(options, registry2) {
1079
1216
  this.options = options;
1080
1217
  this.registry = registry2;
1081
- this.logger = makeLogger(options.debug ?? false, "[hobots-manager]");
1082
- const config = resolveConfig(options, "[hobots-manager]");
1218
+ this.logger = makeLogger(options.debug ?? false, "[hobots]");
1219
+ const config = resolveConfig(options, "[hobots]");
1083
1220
  this.api = new ManagerApi({ baseUrl: config.managerBaseUrl, key: config.key, logger: this.logger });
1084
1221
  this.client = options.client;
1085
1222
  this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
@@ -1113,10 +1250,10 @@ var ManagerAgent = class {
1113
1250
  start() {
1114
1251
  if (this.state === "started") return;
1115
1252
  if (this.state === "stopping") {
1116
- throw new Error("[hobots-manager] start() chamado durante stop() \u2014 aguarde o stop terminar");
1253
+ throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
1117
1254
  }
1118
1255
  if (this.registry.size === 0) {
1119
- throw new Error("[hobots-manager] nenhuma task registrada \u2014 chame register() antes de start()");
1256
+ throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
1120
1257
  }
1121
1258
  this.state = "started";
1122
1259
  if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
@@ -1198,6 +1335,7 @@ var ManagerAgent = class {
1198
1335
  this.consecutiveFailures = 0;
1199
1336
  this.consecutiveAuthFailures = 0;
1200
1337
  for (const run of response.runs) {
1338
+ if (this.activeRuns.size >= this.concurrency) break;
1201
1339
  this.dispatch(run);
1202
1340
  }
1203
1341
  this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
@@ -1218,7 +1356,7 @@ var ManagerAgent = class {
1218
1356
  }
1219
1357
  this.consecutiveFailures++;
1220
1358
  const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
1221
- const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
1359
+ const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
1222
1360
  this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
1223
1361
  return delay;
1224
1362
  }
@@ -1230,7 +1368,7 @@ var ManagerAgent = class {
1230
1368
  this.activeRuns.add(promise);
1231
1369
  }
1232
1370
  async executeRun(run) {
1233
- const tx = new Transaction(this.client, `run:${run.task}`, "manager.run", true, run.id);
1371
+ const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", true, run.id);
1234
1372
  const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
1235
1373
  this.reporters.add(reporter);
1236
1374
  try {
@@ -1296,11 +1434,11 @@ function toRunError(error) {
1296
1434
  if (error instanceof Error) {
1297
1435
  return {
1298
1436
  type: error.name,
1299
- message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
1300
- stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
1437
+ message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
1438
+ stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
1301
1439
  };
1302
1440
  }
1303
- return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
1441
+ return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
1304
1442
  }
1305
1443
 
1306
1444
  // src/core.ts
@@ -1444,6 +1582,7 @@ export {
1444
1582
  init,
1445
1583
  register,
1446
1584
  resolveConfig,
1585
+ scrubSecrets,
1447
1586
  setContext,
1448
1587
  setExtra,
1449
1588
  setTag,