@zixt/host 0.0.156 → 0.0.158

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/index.js +427 -18
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import { homedir as homedir6 } from "node:os";
28
28
  // package.json
29
29
  var package_default = {
30
30
  name: "@zixt/host",
31
- version: "0.0.156",
31
+ version: "0.0.158",
32
32
  type: "module",
33
33
  exports: {
34
34
  ".": "./src/client.ts",
@@ -14881,6 +14881,23 @@ var BrowserFramePayload = external_exports.object({
14881
14881
  width: external_exports.number().int().min(1).max(3840),
14882
14882
  height: external_exports.number().int().min(1).max(2160)
14883
14883
  }).strict();
14884
+ var BrowserCookie = external_exports.object({
14885
+ name: external_exports.string().min(1).max(256),
14886
+ value: external_exports.string().max(8192),
14887
+ domain: external_exports.string().min(1).max(253),
14888
+ path: external_exports.string().min(1).max(2048),
14889
+ expires: external_exports.number().finite().min(-1).max(253402300799),
14890
+ httpOnly: external_exports.boolean(),
14891
+ secure: external_exports.boolean(),
14892
+ sameSite: external_exports.enum(["Strict", "Lax", "None"]),
14893
+ /** Chromium partition key for CHIPS cookies, when present. */
14894
+ partitionKey: external_exports.string().min(1).max(2048).optional()
14895
+ }).strict();
14896
+ var BROWSER_MAX_SYNCED_COOKIES = 512;
14897
+ var BrowserCookieState = external_exports.object({
14898
+ revision: external_exports.number().int().min(0),
14899
+ cookies: external_exports.array(BrowserCookie).max(BROWSER_MAX_SYNCED_COOKIES)
14900
+ }).strict();
14884
14901
  var inputCoordinate = external_exports.number().min(0).max(16384);
14885
14902
  var BrowserMouseButton = external_exports.enum(["left", "middle", "right"]);
14886
14903
  var BrowserInputEvent = external_exports.discriminatedUnion("kind", [
@@ -14940,7 +14957,7 @@ var BrowserSessionProjection = external_exports.object({
14940
14957
  canStart: external_exports.boolean(),
14941
14958
  /** Member-safe reason when canStart is false. */
14942
14959
  reason: external_exports.string().max(300).optional(),
14943
- /** Present only when active work must use a different Machine-local profile. */
14960
+ /** Present only when active work uses a different local profile Machine. */
14944
14961
  profileContext: BrowserProfileContext.optional()
14945
14962
  }).strict();
14946
14963
  var StartBrowserSessionRequest = external_exports.object({ url: external_exports.string().min(1).max(2e3).optional() }).strict();
@@ -18338,7 +18355,7 @@ var ListTasksResponse = external_exports.object({
18338
18355
  }).strict();
18339
18356
 
18340
18357
  // ../../packages/contracts/src/protocol.ts
18341
- var PROTOCOL_VERSION = 13;
18358
+ var PROTOCOL_VERSION = 14;
18342
18359
  var BROWSER_PROFILE_INVENTORY_PAGE_SIZE = 200;
18343
18360
  var HELLO_UNWOUND_ASSIGNMENT_LIMIT = 1e3;
18344
18361
  var TASK_CANCEL_ACK_EVENT = "zixt.task.cancel.acknowledged";
@@ -19512,6 +19529,44 @@ var BrowserSessionEndedFrame = external_exports.object({
19512
19529
  agentId: AgentId,
19513
19530
  reason: BrowserSessionEndReason
19514
19531
  });
19532
+ var BrowserCookieSyncFrame = external_exports.object({
19533
+ type: external_exports.literal("browser.cookies.sync"),
19534
+ requestId: external_exports.string().min(1).max(200),
19535
+ projectId: ProjectId,
19536
+ taskId: TaskId,
19537
+ agentId: AgentId,
19538
+ browserSessionId: external_exports.string().min(1).max(200),
19539
+ /** Omitted only by a Machine that has not received the canonical jar yet. */
19540
+ baseRevision: external_exports.number().int().min(0).optional(),
19541
+ cookies: external_exports.array(BrowserCookie).max(BROWSER_MAX_SYNCED_COOKIES)
19542
+ }).strict();
19543
+ var BrowserCookieSyncResultFrame = external_exports.discriminatedUnion("ok", [
19544
+ external_exports.object({
19545
+ type: external_exports.literal("browser.cookies.result"),
19546
+ requestId: external_exports.string().min(1).max(200),
19547
+ projectId: ProjectId,
19548
+ taskId: TaskId,
19549
+ agentId: AgentId,
19550
+ browserSessionId: external_exports.string().min(1).max(200),
19551
+ ok: external_exports.literal(true),
19552
+ /** False asks the Host to reconcile its local changes over this newer jar. */
19553
+ accepted: external_exports.boolean(),
19554
+ state: BrowserCookieState,
19555
+ error: external_exports.never().optional()
19556
+ }).strict(),
19557
+ external_exports.object({
19558
+ type: external_exports.literal("browser.cookies.result"),
19559
+ requestId: external_exports.string().min(1).max(200),
19560
+ projectId: ProjectId,
19561
+ taskId: TaskId,
19562
+ agentId: AgentId,
19563
+ browserSessionId: external_exports.string().min(1).max(200),
19564
+ ok: external_exports.literal(false),
19565
+ accepted: external_exports.never().optional(),
19566
+ state: external_exports.never().optional(),
19567
+ error: external_exports.string().min(1).max(300)
19568
+ }).strict()
19569
+ ]);
19515
19570
  var BrowserCredentialRequestFrame = external_exports.object({
19516
19571
  type: external_exports.literal("browser.credential.request"),
19517
19572
  projectId: ProjectId,
@@ -19939,6 +19994,7 @@ var HostToCloudFrame = external_exports.discriminatedUnion("type", [
19939
19994
  BrowserStateFrame,
19940
19995
  BrowserScreencastFrame,
19941
19996
  BrowserSessionEndedFrame,
19997
+ BrowserCookieSyncFrame,
19942
19998
  BrowserCredentialRequestFrame,
19943
19999
  BrowserProfilePurged,
19944
20000
  PingFrame,
@@ -19946,6 +20002,7 @@ var HostToCloudFrame = external_exports.discriminatedUnion("type", [
19946
20002
  ]);
19947
20003
  var CloudToHostFrame = external_exports.discriminatedUnion("type", [
19948
20004
  HelloAckFrame,
20005
+ BrowserCookieSyncResultFrame,
19949
20006
  DeliverFrame,
19950
20007
  ConnectionProbeFrame,
19951
20008
  ApiOperationCallFrame,
@@ -19970,6 +20027,11 @@ var CLOSE_CODES = {
19970
20027
  revoked: 4004
19971
20028
  };
19972
20029
  var UNSUPPORTED_PROTOCOL_CLOSE_REASON = "unsupported protocol version";
20030
+ function parseUnsupportedProtocolCloseReason(detail) {
20031
+ if (detail === UNSUPPORTED_PROTOCOL_CLOSE_REASON) return { cloudProtocolVersion: null };
20032
+ const match = /^unsupported protocol version; cloud protocol (\d{1,6})$/.exec(detail);
20033
+ return match ? { cloudProtocolVersion: Number(match[1]) } : null;
20034
+ }
19973
20035
  var CLOSE_REASON_MAX_BYTES = 123;
19974
20036
 
19975
20037
  // ../../packages/contracts/src/webhook-automations.ts
@@ -25813,6 +25875,8 @@ var HostClient = class _HostClient {
25813
25875
  operationGrantWaiters = /* @__PURE__ */ new Map();
25814
25876
  /** Project/Task-qualified, socket-generation-bound web_login exchanges. */
25815
25877
  browserCredentialWaiters = /* @__PURE__ */ new Map();
25878
+ /** Project-scoped, socket-generation-bound Browser cookie exchanges. */
25879
+ browserCookieSyncWaiters = /* @__PURE__ */ new Map();
25816
25880
  /** True only after the connected cloud's helloAck advertised the feature. */
25817
25881
  browserFeatureActive = false;
25818
25882
  workingContextFeatureActive = false;
@@ -25978,6 +26042,13 @@ var HostClient = class _HostClient {
25978
26042
  waiter.deny("No sign-in was attempted: this machine lost its connection to Zixt.");
25979
26043
  }
25980
26044
  }
26045
+ rejectBrowserCookieSyncWaiters() {
26046
+ for (const [requestId, waiter] of this.browserCookieSyncWaiters) {
26047
+ clearTimeout(waiter.timer);
26048
+ this.browserCookieSyncWaiters.delete(requestId);
26049
+ waiter.deny("Browser cookie synchronization lost its Zixt connection.");
26050
+ }
26051
+ }
25981
26052
  async unwindAllRuns(reason, stopReason) {
25982
26053
  const runs = [...this.activeRuns.values()];
25983
26054
  const unwindingAssignments = [...this.activeAssignments.values()];
@@ -26017,6 +26088,7 @@ var HostClient = class _HostClient {
26017
26088
  }
26018
26089
  this.rejectOperationGrantWaiters();
26019
26090
  this.rejectBrowserCredentialWaiters();
26091
+ this.rejectBrowserCookieSyncWaiters();
26020
26092
  for (const deadline of this.authorityExpiryTimers.values()) clearTimeout(deadline.timer);
26021
26093
  this.cancels.clear();
26022
26094
  this.secretGrants.clear();
@@ -26315,7 +26387,7 @@ var HostClient = class _HostClient {
26315
26387
  await this.unwindAllRuns("host connection was replaced by a newer client", "client_shutdown");
26316
26388
  return;
26317
26389
  }
26318
- const versionMismatch = code === CLOSE_CODES.protocolError && detail === UNSUPPORTED_PROTOCOL_CLOSE_REASON;
26390
+ const versionMismatch = code === CLOSE_CODES.protocolError && detail !== void 0 && parseUnsupportedProtocolCloseReason(detail) !== null;
26319
26391
  if (versionMismatch) {
26320
26392
  this.opts.onStatus?.("incompatible", detail);
26321
26393
  this.stopped = true;
@@ -26369,10 +26441,46 @@ var HostClient = class _HostClient {
26369
26441
  * frame at a non-advertising cloud would each close the socket outright.
26370
26442
  */
26371
26443
  sendBrowserFrame(frame) {
26372
- if (!this.browserFeatureActive) return;
26444
+ if (!this.browserFeatureActive) return false;
26373
26445
  const projectId = frame.type === "browser.state" ? frame.state.projectId : frame.type === "browser.frame" ? frame.frame.projectId : frame.projectId;
26374
- if (this.purgingProjects.has(projectId)) return;
26375
- this.send(frame);
26446
+ if (this.purgingProjects.has(projectId)) return false;
26447
+ return this.send(frame);
26448
+ }
26449
+ requestBrowserCookieSync(input) {
26450
+ const socket = this.ws;
26451
+ if (!socket || !this.browserFeatureActive || !this.protocolReady) {
26452
+ return Promise.reject(
26453
+ new Error("Browser cookie synchronization has no live Zixt connection.")
26454
+ );
26455
+ }
26456
+ const requestId = crypto.randomUUID();
26457
+ return new Promise((resolve24, reject3) => {
26458
+ const timer = setTimeout(() => {
26459
+ if (this.browserCookieSyncWaiters.delete(requestId)) {
26460
+ reject3(new Error("Zixt did not answer Browser cookie synchronization in time."));
26461
+ }
26462
+ }, 3e4);
26463
+ timer.unref?.();
26464
+ this.browserCookieSyncWaiters.set(requestId, {
26465
+ projectId: input.projectId,
26466
+ taskId: input.taskId,
26467
+ agentId: input.agentId,
26468
+ browserSessionId: input.browserSessionId,
26469
+ socket,
26470
+ timer,
26471
+ accept: resolve24,
26472
+ deny: (reason) => reject3(new Error(reason))
26473
+ });
26474
+ if (!this.sendBrowserFrame({
26475
+ type: "browser.cookies.sync",
26476
+ requestId,
26477
+ ...input
26478
+ })) {
26479
+ clearTimeout(timer);
26480
+ this.browserCookieSyncWaiters.delete(requestId);
26481
+ reject3(new Error("Browser cookie synchronization has no live Zixt connection."));
26482
+ }
26483
+ });
26376
26484
  }
26377
26485
  replaceablePendingUp(message) {
26378
26486
  return message.type === "host.report" || message.type === "task.working_context" || message.type === "task.runner_runtime" || message.type === "task.title" || message.type === "task.event" && message.event.ephemeral === true;
@@ -26594,7 +26702,8 @@ var HostClient = class _HostClient {
26594
26702
  taskId,
26595
26703
  agentId,
26596
26704
  reason
26597
- })
26705
+ }),
26706
+ syncCookies: (input) => this.requestBrowserCookieSync(input)
26598
26707
  });
26599
26708
  } else {
26600
26709
  this.opts.browser?.attach(null);
@@ -26708,6 +26817,17 @@ var HostClient = class _HostClient {
26708
26817
  waiter.accept(grant);
26709
26818
  return;
26710
26819
  }
26820
+ case "browser.cookies.result": {
26821
+ const waiter = this.browserCookieSyncWaiters.get(frame.requestId);
26822
+ if (!waiter || waiter.socket !== sourceSocket || waiter.projectId !== frame.projectId || waiter.taskId !== frame.taskId || waiter.agentId !== frame.agentId || waiter.browserSessionId !== frame.browserSessionId) {
26823
+ return;
26824
+ }
26825
+ clearTimeout(waiter.timer);
26826
+ this.browserCookieSyncWaiters.delete(frame.requestId);
26827
+ if (frame.ok) waiter.accept({ accepted: frame.accepted, state: frame.state });
26828
+ else waiter.deny(frame.error);
26829
+ return;
26830
+ }
26711
26831
  case "browser.open": {
26712
26832
  if (!this.acceptsVolatileProjectWork(frame.projectId, frame.projectWorkFenceRevision)) {
26713
26833
  return;
@@ -30384,6 +30504,29 @@ function installedReleaseVersion(entry, root = versionsRoot()) {
30384
30504
  if (!version2 || !VERSION_DIR.test(version2)) return null;
30385
30505
  return resolve8(entry) === resolve8(installedReleaseEntry(version2, root)) ? version2 : null;
30386
30506
  }
30507
+ function compareReleaseVersions(left, right) {
30508
+ const parse4 = (version2) => version2.split(/[-+]/, 1)[0].split(".").map((part) => Number(part));
30509
+ const leftParts = parse4(left);
30510
+ const rightParts = parse4(right);
30511
+ for (let index = 0; index < 3; index += 1) {
30512
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
30513
+ if (difference !== 0) return difference;
30514
+ }
30515
+ return 0;
30516
+ }
30517
+ async function newestInstalledReleaseBelow(version2, excluded, root = versionsRoot()) {
30518
+ let names;
30519
+ try {
30520
+ names = await readdir6(root);
30521
+ } catch {
30522
+ return null;
30523
+ }
30524
+ const candidates = names.filter((name) => VERSION_DIR.test(name)).filter((name) => !excluded.has(name) && compareReleaseVersions(name, version2) < 0).sort(compareReleaseVersions).reverse();
30525
+ for (const candidate of candidates) {
30526
+ if (await validInstalledRelease(candidate, root)) return candidate;
30527
+ }
30528
+ return null;
30529
+ }
30387
30530
  function currentReleaseEntry(root = versionsRoot(), platform = process.platform) {
30388
30531
  return join12(root, platform === "win32" ? "current-launcher.cjs" : CURRENT_RELEASE_ENTRY);
30389
30532
  }
@@ -31488,6 +31631,7 @@ async function superviseHost(options = {}) {
31488
31631
  });
31489
31632
  const attempted = /* @__PURE__ */ new Set();
31490
31633
  let unsatisfiableUpdates = 0;
31634
+ const cloudRefusedVersions = /* @__PURE__ */ new Set();
31491
31635
  const waitOrShutdown = async (ms) => {
31492
31636
  if (shuttingDown2) return false;
31493
31637
  if (!customDelay) {
@@ -31568,6 +31712,14 @@ async function superviseHost(options = {}) {
31568
31712
  rollbackCandidate = null;
31569
31713
  }
31570
31714
  };
31715
+ const parkRefusedCandidate = async (version2) => {
31716
+ if (!releaseStore || releaseState?.candidateVersion !== version2) return;
31717
+ await releaseStore.clear();
31718
+ if (releaseState?.candidateVersion === version2) {
31719
+ releaseState = null;
31720
+ rollbackCandidate = null;
31721
+ }
31722
+ };
31571
31723
  const markReleaseValidated = async (version2) => {
31572
31724
  if (releaseState?.phase !== "probation" || releaseState.candidateVersion !== version2 || !releaseStore) {
31573
31725
  return;
@@ -31876,21 +32028,66 @@ async function superviseHost(options = {}) {
31876
32028
  const target = await published();
31877
32029
  let updateLanded = false;
31878
32030
  let rejectedUpdate = null;
32031
+ if (runtimeMs >= FAST_UPDATE_EXIT_MS) cloudRefusedVersions.clear();
31879
32032
  if (target === null) {
31880
32033
  log2(
31881
32034
  "Zixt Host: a newer release was reported but the registry is unreachable; staying on the current version"
31882
32035
  );
31883
32036
  } else if (target === command.version) {
32037
+ cloudRefusedVersions.add(target);
32038
+ await parkRefusedCandidate(target);
31884
32039
  if (unsatisfiableUpdates === 0) {
31885
32040
  log2(
31886
32041
  `Zixt Host: ${target} is the newest published Zixt Host, so no update can satisfy this cloud; the cloud this Machine is paired to speaks a newer Host protocol than any published release`
31887
32042
  );
32043
+ }
32044
+ const compatible = await newestInstalledReleaseBelow(target, cloudRefusedVersions);
32045
+ if (compatible) {
32046
+ log2(
32047
+ `Zixt Host: running installed ${compatible} while the cloud catches up with ${target}; this Machine returns to ${target} by itself once the cloud speaks its protocol`
32048
+ );
32049
+ command = {
32050
+ entry: installedReleaseEntry(compatible),
32051
+ version: compatible,
32052
+ // The parked newest release is not for this worker to re-request:
32053
+ // its own update checks skip it while the cloud stays behind.
32054
+ rejectedVersion: target
32055
+ };
32056
+ updateLanded = true;
32057
+ } else if (unsatisfiableUpdates === 0) {
31888
32058
  log2(
31889
- "Zixt Host: pair this Machine with a cloud running a published release, or start the Host from the same source checkout that cloud runs; this Machine connects by itself once a matching release is published"
32059
+ "Zixt Host: no older installed release remains to serve this cloud; pair this Machine with a cloud running a published release, or start the Host from the same source checkout that cloud runs; this Machine connects by itself once a matching release is published"
32060
+ );
32061
+ }
32062
+ } else if (cloudRefusedVersions.has(target)) {
32063
+ if (command.version && runtimeMs < FAST_UPDATE_EXIT_MS) {
32064
+ cloudRefusedVersions.add(command.version);
32065
+ await parkRefusedCandidate(command.version);
32066
+ }
32067
+ const compatible = command.version ? await newestInstalledReleaseBelow(command.version, cloudRefusedVersions) : null;
32068
+ if (compatible) {
32069
+ log2(
32070
+ `Zixt Host: ${target} is still refused by the paired cloud; running installed ${compatible} until the cloud updates`
32071
+ );
32072
+ command = {
32073
+ entry: installedReleaseEntry(compatible),
32074
+ version: compatible,
32075
+ rejectedVersion: target
32076
+ };
32077
+ updateLanded = true;
32078
+ } else {
32079
+ log2(
32080
+ `Zixt Host: the paired cloud refuses every installed release; waiting for the cloud or a newer published release`
31890
32081
  );
31891
32082
  }
31892
32083
  } else if (attempted.has(target)) {
31893
- log2(`Zixt Host: already running ${target}; ignoring a repeated update request`);
32084
+ if (command.version !== target && await validInstalledRelease(target)) {
32085
+ log2(`Zixt Host: returning to installed ${target} now that the cloud accepts it`);
32086
+ command = { entry: installedReleaseEntry(target), version: target };
32087
+ updateLanded = true;
32088
+ } else {
32089
+ log2(`Zixt Host: already running ${target}; ignoring a repeated update request`);
32090
+ }
31894
32091
  } else {
31895
32092
  let entry;
31896
32093
  const attempt = { failure: null };
@@ -32486,6 +32683,7 @@ function createDemoBrowserAdapterFactory() {
32486
32683
  const tabs = [newDemoTab()];
32487
32684
  let activeIndex = 0;
32488
32685
  let sink = null;
32686
+ let cookies = [];
32489
32687
  const listeners = [];
32490
32688
  const frame = () => ({
32491
32689
  jpegBase64: DEMO_FRAME_JPEG_BASE64,
@@ -32629,6 +32827,12 @@ function createDemoBrowserAdapterFactory() {
32629
32827
  }
32630
32828
  emit();
32631
32829
  },
32830
+ async cookies() {
32831
+ return cookies.map((cookie) => ({ ...cookie }));
32832
+ },
32833
+ async replaceCookies(next) {
32834
+ cookies = next.map((cookie) => ({ ...cookie }));
32835
+ },
32632
32836
  async close() {
32633
32837
  sink = null;
32634
32838
  }
@@ -44384,6 +44588,10 @@ var BrowserManager = class {
44384
44588
  deniedAuthorizations = /* @__PURE__ */ new Map();
44385
44589
  /** Serializes profile lifecycle and session open/close per teammate. */
44386
44590
  locks = /* @__PURE__ */ new Map();
44591
+ /** Last cloud-canonical jar observed for each exact Project teammate. */
44592
+ cookieStates = /* @__PURE__ */ new Map();
44593
+ cookieSyncTails = /* @__PURE__ */ new Map();
44594
+ cookieSyncTimers = /* @__PURE__ */ new Map();
44387
44595
  events = null;
44388
44596
  profileRoot;
44389
44597
  profileStateRoot;
@@ -44400,8 +44608,15 @@ var BrowserManager = class {
44400
44608
  attach(events) {
44401
44609
  this.events = events;
44402
44610
  if (!events) return;
44611
+ const scheduledProfiles = /* @__PURE__ */ new Set();
44403
44612
  for (const session of this.sessions.values()) {
44404
- if (!session.closed) events.state(this.stateOf(session));
44613
+ if (session.closed) continue;
44614
+ events.state(this.stateOf(session));
44615
+ const profileKey = this.profileKey(session.projectId, session.agentId);
44616
+ if (!scheduledProfiles.has(profileKey)) {
44617
+ scheduledProfiles.add(profileKey);
44618
+ this.scheduleCookieSync(session, 0);
44619
+ }
44405
44620
  }
44406
44621
  }
44407
44622
  capability() {
@@ -44486,6 +44701,39 @@ var BrowserManager = class {
44486
44701
  profileKey(projectId, agentId) {
44487
44702
  return `${projectId}:${agentId}`;
44488
44703
  }
44704
+ cookieKey(cookie) {
44705
+ return `${cookie.name}\0${cookie.domain}\0${cookie.path}\0${cookie.partitionKey ?? ""}`;
44706
+ }
44707
+ normalizedCookies(cookies) {
44708
+ return [
44709
+ ...new Map(cookies.map((cookie) => [this.cookieKey(cookie), { ...cookie }])).values()
44710
+ ].sort((left, right) => this.cookieKey(left).localeCompare(this.cookieKey(right)));
44711
+ }
44712
+ cookiesEqual(left, right) {
44713
+ return JSON.stringify(this.normalizedCookies(left)) === JSON.stringify(this.normalizedCookies(right));
44714
+ }
44715
+ /**
44716
+ * Rebase only the changes this Machine made since its last canonical jar
44717
+ * over the newer jar returned by another Machine. Unchanged cookies retain
44718
+ * the remote value; local additions, changes, and deletions win per cookie.
44719
+ */
44720
+ mergeCookieChanges(base, local, remote) {
44721
+ const baseByKey = new Map(base.map((cookie) => [this.cookieKey(cookie), cookie]));
44722
+ const localByKey = new Map(local.map((cookie) => [this.cookieKey(cookie), cookie]));
44723
+ const merged = new Map(remote.map((cookie) => [this.cookieKey(cookie), { ...cookie }]));
44724
+ for (const [key, baseCookie] of baseByKey) {
44725
+ const localCookie = localByKey.get(key);
44726
+ if (!localCookie) {
44727
+ merged.delete(key);
44728
+ } else if (!this.cookiesEqual([baseCookie], [localCookie])) {
44729
+ merged.set(key, { ...localCookie });
44730
+ }
44731
+ }
44732
+ for (const [key, localCookie] of localByKey) {
44733
+ if (!baseByKey.has(key)) merged.set(key, { ...localCookie });
44734
+ }
44735
+ return this.normalizedCookies([...merged.values()]);
44736
+ }
44489
44737
  exactChild(root, child) {
44490
44738
  const canonicalRoot = resolve16(root);
44491
44739
  const target = resolve16(canonicalRoot, child);
@@ -44606,6 +44854,87 @@ var BrowserManager = class {
44606
44854
  throw error52;
44607
44855
  }
44608
44856
  }
44857
+ synchronizeCookies(session) {
44858
+ const profileKey = this.profileKey(session.projectId, session.agentId);
44859
+ const tail = this.cookieSyncTails.get(profileKey) ?? Promise.resolve();
44860
+ const next = tail.then(async () => {
44861
+ if (session.closed) return;
44862
+ const synchronize = this.events?.syncCookies;
44863
+ if (!synchronize) return;
44864
+ let baseline = this.cookieStates.get(profileKey);
44865
+ let observedLocal = this.normalizedCookies(await session.adapter.cookies());
44866
+ let desired = observedLocal;
44867
+ for (let attempt = 0; attempt < 8; attempt++) {
44868
+ if (desired.length > BROWSER_MAX_SYNCED_COOKIES) {
44869
+ throw new Error(
44870
+ `the browser profile has more than ${BROWSER_MAX_SYNCED_COOKIES} cookies and cannot be synchronized safely`
44871
+ );
44872
+ }
44873
+ const result = await synchronize({
44874
+ projectId: session.projectId,
44875
+ taskId: session.taskId,
44876
+ agentId: session.agentId,
44877
+ browserSessionId: session.browserSessionId,
44878
+ ...baseline ? { baseRevision: baseline.revision } : {},
44879
+ cookies: desired
44880
+ });
44881
+ if (result.accepted) {
44882
+ const canonical = {
44883
+ revision: result.state.revision,
44884
+ cookies: this.normalizedCookies(result.state.cookies)
44885
+ };
44886
+ this.cookieStates.set(profileKey, canonical);
44887
+ const current2 = this.normalizedCookies(await session.adapter.cookies());
44888
+ if (!this.cookiesEqual(current2, observedLocal)) {
44889
+ baseline = canonical;
44890
+ desired = this.mergeCookieChanges(observedLocal, current2, canonical.cookies);
44891
+ observedLocal = current2;
44892
+ continue;
44893
+ }
44894
+ if (!this.cookiesEqual(current2, canonical.cookies)) {
44895
+ await session.adapter.replaceCookies(canonical.cookies);
44896
+ }
44897
+ return;
44898
+ }
44899
+ const remote = {
44900
+ revision: result.state.revision,
44901
+ cookies: this.normalizedCookies(result.state.cookies)
44902
+ };
44903
+ if (!baseline) {
44904
+ this.cookieStates.set(profileKey, remote);
44905
+ await session.adapter.replaceCookies(remote.cookies);
44906
+ return;
44907
+ }
44908
+ const current = this.normalizedCookies(await session.adapter.cookies());
44909
+ const currentDesired = this.cookiesEqual(current, observedLocal) ? desired : this.mergeCookieChanges(observedLocal, current, desired);
44910
+ desired = this.mergeCookieChanges(baseline.cookies, currentDesired, remote.cookies);
44911
+ baseline = remote;
44912
+ observedLocal = current;
44913
+ }
44914
+ throw new Error("browser cookies changed too many times while synchronizing");
44915
+ });
44916
+ const tracked = next.catch(() => {
44917
+ }).finally(() => {
44918
+ if (this.cookieSyncTails.get(profileKey) === tracked)
44919
+ this.cookieSyncTails.delete(profileKey);
44920
+ });
44921
+ this.cookieSyncTails.set(profileKey, tracked);
44922
+ return next;
44923
+ }
44924
+ scheduleCookieSync(session, delayMs = 250) {
44925
+ if (session.closed || !this.events?.syncCookies) return;
44926
+ const profileKey = this.profileKey(session.projectId, session.agentId);
44927
+ const existing = this.cookieSyncTimers.get(profileKey);
44928
+ if (existing) clearTimeout(existing);
44929
+ const timer = setTimeout(() => {
44930
+ if (this.cookieSyncTimers.get(profileKey) !== timer) return;
44931
+ this.cookieSyncTimers.delete(profileKey);
44932
+ void this.synchronizeCookies(session).catch(() => {
44933
+ });
44934
+ }, delayMs);
44935
+ timer.unref?.();
44936
+ this.cookieSyncTimers.set(profileKey, timer);
44937
+ }
44609
44938
  /**
44610
44939
  * Ensure a live session for this Task. An existing session is adopted
44611
44940
  * as-is (the cloud-minted id loses to the live one — the cloud reconciles
@@ -44683,8 +45012,20 @@ var BrowserManager = class {
44683
45012
  closeFailed: false
44684
45013
  };
44685
45014
  this.sessions.set(taskKey, session);
45015
+ try {
45016
+ await this.synchronizeCookies(session);
45017
+ } catch (error52) {
45018
+ session.closed = true;
45019
+ this.sessions.delete(taskKey);
45020
+ await adapter.close().catch(() => {
45021
+ });
45022
+ throw error52;
45023
+ }
44686
45024
  adapter.onStateChanged(() => {
44687
- if (!session.closed) this.events?.state(this.stateOf(session));
45025
+ if (!session.closed) {
45026
+ this.events?.state(this.stateOf(session));
45027
+ this.scheduleCookieSync(session);
45028
+ }
44688
45029
  });
44689
45030
  this.armIdleTimer(session);
44690
45031
  this.events?.state(this.stateOf(session));
@@ -44714,6 +45055,14 @@ var BrowserManager = class {
44714
45055
  return;
44715
45056
  }
44716
45057
  if (!session || session.closed && !session.closeFailed) return;
45058
+ const profileKey = this.profileKey(session.projectId, session.agentId);
45059
+ const pendingCookieTimer = this.cookieSyncTimers.get(profileKey);
45060
+ if (pendingCookieTimer) {
45061
+ clearTimeout(pendingCookieTimer);
45062
+ this.cookieSyncTimers.delete(profileKey);
45063
+ }
45064
+ await this.synchronizeCookies(session).catch(() => {
45065
+ });
44717
45066
  session.closed = true;
44718
45067
  session.closeFailed = false;
44719
45068
  if (session.frameTimer) clearTimeout(session.frameTimer);
@@ -44763,6 +45112,12 @@ var BrowserManager = class {
44763
45112
  force: true,
44764
45113
  maxRetries: 3
44765
45114
  });
45115
+ const profileKey = this.profileKey(projectId, agentId);
45116
+ const timer = this.cookieSyncTimers.get(profileKey);
45117
+ if (timer) clearTimeout(timer);
45118
+ this.cookieSyncTimers.delete(profileKey);
45119
+ this.cookieSyncTails.delete(profileKey);
45120
+ this.cookieStates.delete(profileKey);
44766
45121
  await this.syncDirectory(this.projectProfileRoot(projectId));
44767
45122
  });
44768
45123
  }
@@ -44797,6 +45152,17 @@ var BrowserManager = class {
44797
45152
  this.deniedAuthorizations.forEach((_revision, key) => {
44798
45153
  if (key.startsWith(`${projectId}:`)) this.deniedAuthorizations.delete(key);
44799
45154
  });
45155
+ this.cookieSyncTimers.forEach((timer, key) => {
45156
+ if (!key.startsWith(`${projectId}:`)) return;
45157
+ clearTimeout(timer);
45158
+ this.cookieSyncTimers.delete(key);
45159
+ });
45160
+ this.cookieSyncTails.forEach((_tail, key) => {
45161
+ if (key.startsWith(`${projectId}:`)) this.cookieSyncTails.delete(key);
45162
+ });
45163
+ this.cookieStates.forEach((_state, key) => {
45164
+ if (key.startsWith(`${projectId}:`)) this.cookieStates.delete(key);
45165
+ });
44800
45166
  await this.syncDirectory(this.profileRoot);
44801
45167
  await this.syncDirectory(this.profileStateRoot);
44802
45168
  }
@@ -44865,6 +45231,11 @@ var BrowserManager = class {
44865
45231
  const next = session.inputQueue.then(work, work);
44866
45232
  session.inputQueue = next.catch(() => {
44867
45233
  });
45234
+ void next.then(
45235
+ () => this.scheduleCookieSync(session),
45236
+ () => {
45237
+ }
45238
+ );
44868
45239
  return next;
44869
45240
  }
44870
45241
  /** Tool surface: every call ensures the session and counts as activity. */
@@ -45666,6 +46037,37 @@ function createPlaywrightBrowserAdapterFactory(dependencies = {}) {
45666
46037
  }
45667
46038
  }
45668
46039
  },
46040
+ async cookies() {
46041
+ return (await context.cookies()).map((cookie) => ({
46042
+ name: cookie.name,
46043
+ value: cookie.value,
46044
+ domain: cookie.domain,
46045
+ path: cookie.path,
46046
+ expires: cookie.expires,
46047
+ httpOnly: cookie.httpOnly,
46048
+ secure: cookie.secure,
46049
+ sameSite: cookie.sameSite,
46050
+ ...cookie.partitionKey ? { partitionKey: cookie.partitionKey } : {}
46051
+ }));
46052
+ },
46053
+ async replaceCookies(cookies) {
46054
+ await context.clearCookies();
46055
+ if (cookies.length > 0) {
46056
+ await context.addCookies(
46057
+ cookies.map((cookie) => ({
46058
+ name: cookie.name,
46059
+ value: cookie.value,
46060
+ domain: cookie.domain,
46061
+ path: cookie.path,
46062
+ expires: cookie.expires,
46063
+ httpOnly: cookie.httpOnly,
46064
+ secure: cookie.secure,
46065
+ sameSite: cookie.sameSite,
46066
+ ...cookie.partitionKey !== void 0 ? { partitionKey: cookie.partitionKey } : {}
46067
+ }))
46068
+ );
46069
+ }
46070
+ },
45669
46071
  async close() {
45670
46072
  closed = true;
45671
46073
  for (const tab of [...tabs]) {
@@ -48384,14 +48786,21 @@ var client = new HostClient({
48384
48786
  });
48385
48787
  queueMicrotask(() => shutdown(DO_NOT_RESTART_EXIT_CODE));
48386
48788
  break;
48387
- case "incompatible":
48388
- log.error("Zixt Cloud refused this Host as too old to connect", {
48389
- ...connectionContext,
48390
- protocol: PROTOCOL_VERSION,
48391
- next: packagedBuild ? "Zixt checks the published release now; nothing to do on this Machine" : "Update this checkout through Git and restart the Host"
48392
- });
48789
+ case "incompatible": {
48790
+ const refusal = detail ? parseUnsupportedProtocolCloseReason(detail) : null;
48791
+ const cloudBehind = refusal?.cloudProtocolVersion != null && refusal.cloudProtocolVersion < PROTOCOL_VERSION;
48792
+ log.error(
48793
+ cloudBehind ? "Zixt Cloud is behind this Host and refused the connection" : "Zixt Cloud refused this Host as too old to connect",
48794
+ {
48795
+ ...connectionContext,
48796
+ protocol: PROTOCOL_VERSION,
48797
+ ...refusal?.cloudProtocolVersion != null ? { cloudProtocol: refusal.cloudProtocolVersion } : {},
48798
+ next: packagedBuild ? cloudBehind ? "Zixt runs the newest installed release this cloud accepts until the cloud updates" : "Zixt checks the published release now; nothing to do on this Machine" : cloudBehind ? "Deploy the paired cloud, or run this checkout at the revision that cloud runs" : "Update this checkout through Git and restart the Host"
48799
+ }
48800
+ );
48393
48801
  queueMicrotask(() => shutdown(packagedBuild ? UPDATE_EXIT_CODE : DO_NOT_RESTART_EXIT_CODE));
48394
48802
  break;
48803
+ }
48395
48804
  case "rejected":
48396
48805
  log.error("Zixt Cloud refused this Machine", {
48397
48806
  ...connectionContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zixt/host",
3
- "version": "0.0.156",
3
+ "version": "0.0.158",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/client.ts",