@buda-ai/connector 0.1.26 → 0.1.28

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 (2) hide show
  1. package/dist/cli.js +98 -17
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -523,7 +523,7 @@ import path2 from "path";
523
523
  // package.json
524
524
  var package_default = {
525
525
  name: "@buda-ai/connector",
526
- version: "0.1.26",
526
+ version: "0.1.28",
527
527
  private: false,
528
528
  type: "module",
529
529
  publishConfig: {
@@ -550,7 +550,7 @@ var package_default = {
550
550
  "lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
551
551
  },
552
552
  dependencies: {
553
- "@bunny-agent/daemon": "0.9.56",
553
+ "@bunny-agent/daemon": "0.9.57",
554
554
  "@clack/prompts": "^1.0.0",
555
555
  "playwright-core": "^1.52.0",
556
556
  ws: "^8.21.0"
@@ -2215,6 +2215,13 @@ var ConnectorDeletedError = class extends Error {
2215
2215
  this.name = "ConnectorDeletedError";
2216
2216
  }
2217
2217
  };
2218
+ var ConnectorUnauthorizedError = class extends Error {
2219
+ constructor(message) {
2220
+ super(message);
2221
+ this.name = "ConnectorUnauthorizedError";
2222
+ }
2223
+ };
2224
+ var UNREGISTER_TIMEOUT_MS = 3e3;
2218
2225
  async function resolveConnectorScope(deviceId, launchConfig) {
2219
2226
  const serverUrl = normalizeServerOrigin(getServerUrl(launchConfig));
2220
2227
  const headers = { "Content-Type": "application/json" };
@@ -2301,14 +2308,15 @@ function buildAuthHeaders(config, launchConfig) {
2301
2308
  if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
2302
2309
  return headers;
2303
2310
  }
2304
- async function postJson(serverUrl, pathname, body, config, launchConfig) {
2311
+ async function postJson(serverUrl, pathname, body, config, launchConfig, options = {}) {
2305
2312
  const response = await fetch(`${serverUrl}${pathname}`, {
2306
2313
  method: "POST",
2307
2314
  headers: {
2308
2315
  "Content-Type": "application/json",
2309
2316
  ...buildAuthHeaders(config, launchConfig)
2310
2317
  },
2311
- body: JSON.stringify(body)
2318
+ body: JSON.stringify(body),
2319
+ ...options.timeoutMs ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
2312
2320
  });
2313
2321
  if (!response.ok) {
2314
2322
  const text2 = await response.text().catch(() => "");
@@ -2328,6 +2336,9 @@ async function postJson(serverUrl, pathname, body, config, launchConfig) {
2328
2336
  if (errorCode === "connector_deleted") {
2329
2337
  throw new ConnectorDeletedError(errorMessage3 || "Connector no longer exists");
2330
2338
  }
2339
+ if (response.status === 401) {
2340
+ throw new ConnectorUnauthorizedError(errorMessage3 || "Connector authentication failed");
2341
+ }
2331
2342
  throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
2332
2343
  }
2333
2344
  return await response.json();
@@ -2403,7 +2414,8 @@ async function unregisterConnector(config, launchConfig) {
2403
2414
  connectorId: config.connectorId
2404
2415
  },
2405
2416
  config,
2406
- launchConfig
2417
+ launchConfig,
2418
+ { timeoutMs: UNREGISTER_TIMEOUT_MS }
2407
2419
  ).catch((error) => {
2408
2420
  void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
2409
2421
  });
@@ -2601,6 +2613,7 @@ async function ensureStableConnectorRuntime() {
2601
2613
  // src/daemon-client.ts
2602
2614
  var START_TIMEOUT_MS = 45e3;
2603
2615
  var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
2616
+ var GRACEFUL_STOP_TIMEOUT_MS = 3e3;
2604
2617
  var LOCK_ONLY_RECOVERY_MAX_AGE_MS = 5 * 6e4;
2605
2618
  var POLL_INTERVAL_MS = 250;
2606
2619
  var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
@@ -2754,6 +2767,30 @@ var stopUnresponsiveDaemonProcess = async (pid) => {
2754
2767
  if (await waitForProcessExit(pid, 3e3)) return;
2755
2768
  throw new Error(`Timed-out Connector daemon PID ${pid} could not be stopped safely`);
2756
2769
  };
2770
+ var waitForDaemonRelease = async (config, status, configRoot = getConfigRoot()) => {
2771
+ const deadline = Date.now() + GRACEFUL_STOP_TIMEOUT_MS;
2772
+ while (Date.now() < deadline) {
2773
+ if (!isProcessAlive2(status.pid)) return true;
2774
+ const current = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
2775
+ if (!current || current.pid !== status.pid || current.instanceId !== status.instanceId) {
2776
+ return true;
2777
+ }
2778
+ await wait2(50);
2779
+ }
2780
+ return !isProcessAlive2(status.pid);
2781
+ };
2782
+ var stopLiveDaemon = async (config, status, configRoot = getConfigRoot(), command = "stop") => {
2783
+ const stopStatus = await requestDaemonControl(config, command, configRoot);
2784
+ if (command === "stop-if-idle" && !stopStatus.idle) return false;
2785
+ if (await waitForDaemonRelease(config, status, configRoot)) return true;
2786
+ const current = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
2787
+ if (!current || current.pid !== status.pid || current.instanceId !== status.instanceId) {
2788
+ return true;
2789
+ }
2790
+ await stopUnresponsiveDaemonProcess(status.pid);
2791
+ return true;
2792
+ };
2793
+ var shouldRestartUnhealthyDaemonForEnrollment = (status, explicitEnrollment) => explicitEnrollment && status.idle && status.heartbeatHealthy === false;
2757
2794
  var readDaemonLockOwnerPid = async (config) => {
2758
2795
  const lockPath = `${getConnectorDaemonLockTarget(
2759
2796
  config.serverUrl,
@@ -2817,6 +2854,12 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
2817
2854
  if (existingConfig) {
2818
2855
  const existingStatus = await readLiveStatus(existingConfig);
2819
2856
  if (existingStatus) {
2857
+ if (shouldRestartUnhealthyDaemonForEnrollment(
2858
+ existingStatus,
2859
+ Boolean(options.explicitEnrollment)
2860
+ ) && await stopLiveDaemon(existingConfig, existingStatus, getConfigRoot(), "stop-if-idle")) {
2861
+ return await startInternalDaemonWithRecovery(launchConfig, mode, void 0, options);
2862
+ }
2820
2863
  const preparedRuntime = !shouldReplaceDaemonVersion(existingStatus.version, CONNECTOR_RUNTIME_VERSION) || !existingStatus.idle ? void 0 : await ensureStableConnectorRuntime();
2821
2864
  const replaced = await stopOlderIdleDaemon(existingConfig, existingStatus);
2822
2865
  if (!replaced) {
@@ -2937,7 +2980,7 @@ async function stopLocalConnectorDaemon(connectorId, serverUrl, configRoot = get
2937
2980
  if (!config) throw new Error(`Local Connector ${connectorId} was not found`);
2938
2981
  const status = await readLiveStatus(config, configRoot);
2939
2982
  if (!status) return;
2940
- await requestDaemonControl(config, "stop", configRoot);
2983
+ await stopLiveDaemon(config, status, configRoot);
2941
2984
  }
2942
2985
  async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
2943
2986
  const configs = await listDeviceConfigs(configRoot);
@@ -2950,7 +2993,7 @@ ${config.connectorId}`, config);
2950
2993
  const results = await Promise.allSettled(
2951
2994
  [...identities.values()].map(async (config) => {
2952
2995
  const status = await readLiveStatus(config, configRoot);
2953
- if (status) await requestDaemonControl(config, "stop", configRoot);
2996
+ if (status) await stopLiveDaemon(config, status, configRoot);
2954
2997
  })
2955
2998
  );
2956
2999
  const failures = results.filter((result) => result.status === "rejected");
@@ -5743,7 +5786,30 @@ import { mkdir as mkdir12 } from "fs/promises";
5743
5786
  import path15 from "path";
5744
5787
  var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
5745
5788
  var RECOVERED_DAEMON_STOP_POLL_MS = 100;
5789
+ var SHUTDOWN_FORCE_EXIT_MS = 1e4;
5790
+ var SHUTDOWN_STEP_TIMEOUT_MS = 2e3;
5746
5791
  var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
5792
+ var runShutdownStep = async (label, operation) => {
5793
+ let timeout;
5794
+ try {
5795
+ await Promise.race([
5796
+ Promise.resolve().then(operation),
5797
+ new Promise((_, reject) => {
5798
+ timeout = setTimeout(
5799
+ () => reject(new Error(`${label} timed out after ${SHUTDOWN_STEP_TIMEOUT_MS}ms`)),
5800
+ SHUTDOWN_STEP_TIMEOUT_MS
5801
+ );
5802
+ })
5803
+ ]);
5804
+ } catch (error) {
5805
+ await writeLog(
5806
+ "warn",
5807
+ `${label} failed during shutdown: ${error instanceof Error ? error.message : String(error)}`
5808
+ ).catch(() => void 0);
5809
+ } finally {
5810
+ if (timeout) clearTimeout(timeout);
5811
+ }
5812
+ };
5747
5813
  async function stopRecoveredIdleDaemon(config, configRoot) {
5748
5814
  const stopStatus = await requestDaemonControl(config, "stop-if-idle", configRoot).catch(
5749
5815
  () => null
@@ -5863,18 +5929,30 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5863
5929
  const shutdown = async (shutdownOptions = {}) => {
5864
5930
  if (shuttingDown) return;
5865
5931
  shuttingDown = true;
5932
+ const forceExit = options.exitOnShutdown === false ? void 0 : setTimeout(() => process.exit(0), SHUTDOWN_FORCE_EXIT_MS);
5866
5933
  if (interval) clearInterval(interval);
5867
5934
  relay.stop();
5868
5935
  try {
5869
- await closeAllBrowserSessions();
5870
- if (shutdownOptions.unregister !== false) {
5871
- await unregisterConnector(config, launchConfig);
5872
- }
5873
- await stopEmbeddedBunnyDaemon(activeBunnyDaemon);
5936
+ await Promise.all([
5937
+ runShutdownStep("browser session cleanup", closeAllBrowserSessions),
5938
+ ...shutdownOptions.unregister === false ? [] : [
5939
+ runShutdownStep(
5940
+ "connector unregister",
5941
+ () => unregisterConnector(config, launchConfig)
5942
+ )
5943
+ ],
5944
+ runShutdownStep(
5945
+ "embedded Bunny daemon shutdown",
5946
+ () => stopEmbeddedBunnyDaemon(activeBunnyDaemon)
5947
+ )
5948
+ ]);
5874
5949
  } finally {
5875
- await control?.close();
5876
- await daemonLock.release();
5877
- await restoreConsole?.();
5950
+ await Promise.all([
5951
+ runShutdownStep("control channel shutdown", async () => control?.close()),
5952
+ runShutdownStep("daemon lock release", () => daemonLock.release()),
5953
+ runShutdownStep("console capture restore", async () => restoreConsole?.())
5954
+ ]);
5955
+ if (forceExit) clearTimeout(forceExit);
5878
5956
  if (options.exitOnShutdown !== false) process.exit(0);
5879
5957
  }
5880
5958
  };
@@ -5889,8 +5967,11 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5889
5967
  });
5890
5968
  }).catch((error) => {
5891
5969
  heartbeatHealthy = false;
5892
- if (error instanceof ConnectorDeletedError) {
5893
- void writeLog("warn", "connector deleted remotely; stopping daemon");
5970
+ if (error instanceof ConnectorDeletedError || error instanceof ConnectorUnauthorizedError) {
5971
+ void writeLog(
5972
+ "warn",
5973
+ error instanceof ConnectorDeletedError ? "connector deleted remotely; stopping daemon" : "connector authentication rejected remotely; stopping daemon"
5974
+ );
5894
5975
  void shutdown({ unregister: false });
5895
5976
  return;
5896
5977
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buda-ai/connector",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -27,7 +27,7 @@
27
27
  "lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
28
28
  },
29
29
  "dependencies": {
30
- "@bunny-agent/daemon": "0.9.56",
30
+ "@bunny-agent/daemon": "0.9.57",
31
31
  "@clack/prompts": "^1.0.0",
32
32
  "playwright-core": "^1.52.0",
33
33
  "ws": "^8.21.0"