supbuddy 3.1.18 → 3.1.23

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/bin.js CHANGED
@@ -19207,6 +19207,12 @@ function startRelayServer(child, opts) {
19207
19207
  let hostBridgeOwner = null;
19208
19208
  let seq2 = 0;
19209
19209
  const pending = /* @__PURE__ */ new Map();
19210
+ const sendOwnerReady = () => {
19211
+ try {
19212
+ child.send?.({ type: "host-bridge:owner-ready" });
19213
+ } catch {
19214
+ }
19215
+ };
19210
19216
  const failAllPending = (reason) => {
19211
19217
  for (const [, p] of pending) {
19212
19218
  clearTimeout(p.timer);
@@ -19225,17 +19231,11 @@ function startRelayServer(child, opts) {
19225
19231
  }
19226
19232
  });
19227
19233
  socket2.on("host-bridge:claim", () => {
19228
- const hadOwner = hostBridgeOwner !== null;
19229
19234
  if (hostBridgeOwner && hostBridgeOwner.id !== socket2.id) {
19230
19235
  hostBridgeOwner.emit("host-bridge:revoked");
19231
19236
  }
19232
19237
  hostBridgeOwner = socket2;
19233
- if (!hadOwner) {
19234
- try {
19235
- child.send?.({ type: "host-bridge:owner-ready" });
19236
- } catch {
19237
- }
19238
- }
19238
+ sendOwnerReady();
19239
19239
  });
19240
19240
  socket2.on("host-bridge:reply", ({ correlationId, reply }) => {
19241
19241
  const p = pending.get(correlationId);
@@ -19265,6 +19265,10 @@ function startRelayServer(child, opts) {
19265
19265
  });
19266
19266
  };
19267
19267
  const onChildMessage = (m) => {
19268
+ if (m?.type === "host-bridge:owner?") {
19269
+ if (hostBridgeOwner) sendOwnerReady();
19270
+ return;
19271
+ }
19268
19272
  if (isHostBridgeMessage(m)) return;
19269
19273
  io2.emit("worker:message", m);
19270
19274
  };
@@ -19803,7 +19807,7 @@ function cliBuildKind(env3 = process.env) {
19803
19807
  return raw === "host" || raw === "npm" ? raw : "dev";
19804
19808
  }
19805
19809
  function cliVersion(env3 = process.env) {
19806
- return "3.1.18".trim() || "0.0.0-dev";
19810
+ return "3.1.23".trim() || "0.0.0-dev";
19807
19811
  }
19808
19812
  function isDevBuild(env3) {
19809
19813
  return cliBuildKind(env3) === "dev";
@@ -28192,18 +28196,39 @@ var init_store = __esm({
28192
28196
  }));
28193
28197
  },
28194
28198
  setDoctorSummary: (s, report) => set2({ doctorSummary: s, doctorReport: report }),
28199
+ // ── A WRITE THAT CHANGES NOTHING MUST NOT LOOK LIKE A CHANGE ───────────────────
28200
+ //
28201
+ // Both setters used to build a NEW proxyState object unconditionally, so writing the
28202
+ // value that was already there still produced a fresh reference. The store subscriber
28203
+ // publishes on `state.proxyState !== prevState.proxyState`, so a no-op write emitted
28204
+ // `proxy-state-updated` to every client.
28205
+ //
28206
+ // That turned a read into a loop. `port-forwarding:status` — a QUERY — ends by calling
28207
+ // `setPortForwardingEnabled(status.enabled)`, almost always with the value already
28208
+ // held. The renderer re-probed whenever proxyState changed, so: poll → no-op write →
28209
+ // emit → poll. Bounded only by IPC latency, which measured at **51 probes per second**,
28210
+ // each opening a TCP connection to the redirected :443. Under that self-inflicted load
28211
+ // some connects timed out, and each timeout was painted as `NOT ENFORCING` on a machine
28212
+ // answering 30 of 30 sequential probes.
28213
+ //
28214
+ // The renderer's dependency was also fixed, but this is the half that matters: no UI
28215
+ // should be able to induce a feedback loop by reading status. Equality-guard the write,
28216
+ // and the loop cannot form.
28195
28217
  setPortForwardingEnabled: (enabled) => {
28196
- set2((state) => ({
28197
- proxyState: {
28198
- ...state.proxyState,
28199
- portForwardingEnabled: enabled
28200
- }
28201
- }));
28218
+ set2((state) => state.proxyState.portForwardingEnabled === enabled ? {} : { proxyState: { ...state.proxyState, portForwardingEnabled: enabled } });
28202
28219
  },
28203
28220
  // Patch arbitrary proxy fields (restartAttempt/lastExit/networkingDegraded/…) without
28204
28221
  // touching status — used by the supervisor + the drift-gated networking repair.
28222
+ //
28223
+ // Same guard, generalised: the supervisor and the health paths call this on a timer
28224
+ // with values that are usually unchanged, and each such call was a broadcast.
28205
28225
  setProxyFields: (fields) => {
28206
- set2((state) => ({ proxyState: { ...state.proxyState, ...fields } }));
28226
+ set2((state) => {
28227
+ const current = state.proxyState;
28228
+ const changed = Object.entries(fields).some(([k, v]) => current[k] !== v);
28229
+ if (!changed) return {};
28230
+ return { proxyState: { ...state.proxyState, ...fields } };
28231
+ });
28207
28232
  },
28208
28233
  updateSettings: (updates) => {
28209
28234
  set2((state) => ({
@@ -39154,7 +39179,10 @@ async function probe443(timeoutMs = 1e3) {
39154
39179
  }
39155
39180
  async function probePort(port, timeoutMs = 1e3) {
39156
39181
  const results = await Promise.all(["127.0.0.1", "::1"].map((host) => probeHost(host, port, timeoutMs)));
39157
- return results.some(Boolean);
39182
+ if (results.some(Boolean)) return true;
39183
+ await new Promise((r) => setTimeout(r, 150));
39184
+ const confirm = await Promise.all(["127.0.0.1", "::1"].map((host) => probeHost(host, port, timeoutMs)));
39185
+ return confirm.some(Boolean);
39158
39186
  }
39159
39187
  async function probeHost(host, port, timeoutMs) {
39160
39188
  const net2 = await import("net");
@@ -26296,18 +26296,39 @@ const useStore = create((set2, get2) => ({
26296
26296
  }));
26297
26297
  },
26298
26298
  setDoctorSummary: (s, report) => set2({ doctorSummary: s, doctorReport: report }),
26299
+ // ── A WRITE THAT CHANGES NOTHING MUST NOT LOOK LIKE A CHANGE ───────────────────
26300
+ //
26301
+ // Both setters used to build a NEW proxyState object unconditionally, so writing the
26302
+ // value that was already there still produced a fresh reference. The store subscriber
26303
+ // publishes on `state.proxyState !== prevState.proxyState`, so a no-op write emitted
26304
+ // `proxy-state-updated` to every client.
26305
+ //
26306
+ // That turned a read into a loop. `port-forwarding:status` — a QUERY — ends by calling
26307
+ // `setPortForwardingEnabled(status.enabled)`, almost always with the value already
26308
+ // held. The renderer re-probed whenever proxyState changed, so: poll → no-op write →
26309
+ // emit → poll. Bounded only by IPC latency, which measured at **51 probes per second**,
26310
+ // each opening a TCP connection to the redirected :443. Under that self-inflicted load
26311
+ // some connects timed out, and each timeout was painted as `NOT ENFORCING` on a machine
26312
+ // answering 30 of 30 sequential probes.
26313
+ //
26314
+ // The renderer's dependency was also fixed, but this is the half that matters: no UI
26315
+ // should be able to induce a feedback loop by reading status. Equality-guard the write,
26316
+ // and the loop cannot form.
26299
26317
  setPortForwardingEnabled: (enabled) => {
26300
- set2((state) => ({
26301
- proxyState: {
26302
- ...state.proxyState,
26303
- portForwardingEnabled: enabled
26304
- }
26305
- }));
26318
+ set2((state) => state.proxyState.portForwardingEnabled === enabled ? {} : { proxyState: { ...state.proxyState, portForwardingEnabled: enabled } });
26306
26319
  },
26307
26320
  // Patch arbitrary proxy fields (restartAttempt/lastExit/networkingDegraded/…) without
26308
26321
  // touching status — used by the supervisor + the drift-gated networking repair.
26322
+ //
26323
+ // Same guard, generalised: the supervisor and the health paths call this on a timer
26324
+ // with values that are usually unchanged, and each such call was a broadcast.
26309
26325
  setProxyFields: (fields) => {
26310
- set2((state) => ({ proxyState: { ...state.proxyState, ...fields } }));
26326
+ set2((state) => {
26327
+ const current = state.proxyState;
26328
+ const changed = Object.entries(fields).some(([k, v]) => current[k] !== v);
26329
+ if (!changed) return {};
26330
+ return { proxyState: { ...state.proxyState, ...fields } };
26331
+ });
26311
26332
  },
26312
26333
  updateSettings: (updates) => {
26313
26334
  set2((state) => ({
@@ -30248,7 +30269,10 @@ async function probe443(timeoutMs = 1e3) {
30248
30269
  }
30249
30270
  async function probePort(port, timeoutMs = 1e3) {
30250
30271
  const results = await Promise.all(["127.0.0.1", "::1"].map((host) => probeHost(host, port, timeoutMs)));
30251
- return results.some(Boolean);
30272
+ if (results.some(Boolean)) return true;
30273
+ await new Promise((r) => setTimeout(r, 150));
30274
+ const confirm2 = await Promise.all(["127.0.0.1", "::1"].map((host) => probeHost(host, port, timeoutMs)));
30275
+ return confirm2.some(Boolean);
30252
30276
  }
30253
30277
  async function probeHost(host, port, timeoutMs) {
30254
30278
  const net2 = await __vitePreload(() => import("net"), false ? __VITE_PRELOAD__ : void 0);
@@ -51581,9 +51605,15 @@ async function requestPrivilegedBatch(command, prompt, tmpFiles = []) {
51581
51605
  process.on("message", handler);
51582
51606
  });
51583
51607
  }
51608
+ let privilegedRepairAttempted = false;
51584
51609
  let guiProxyAutoStartDone = false;
51585
51610
  let lastPrivilegedError = null;
51611
+ let privilegedWorkPending = null;
51586
51612
  const TRANSIENT_PRIVILEGED_FAILURE = /TTY|ASKPASS|detached/i;
51613
+ function shouldFinishPrivilegedSetup(proxyState, privilegedError, workPending) {
51614
+ if (shouldRetryPrivilegedSetup(proxyState, privilegedError)) return true;
51615
+ return TRANSIENT_PRIVILEGED_FAILURE.test(workPending || "");
51616
+ }
51587
51617
  function shouldRetryPrivilegedSetup(proxyState, privilegedError) {
51588
51618
  if (!proxyState.needsPrivilegedRepair) return false;
51589
51619
  return TRANSIENT_PRIVILEGED_FAILURE.test(privilegedError || "");
@@ -51594,14 +51624,21 @@ function isPfRulesetRejection(err, stderr) {
51594
51624
  return stderr.trim().length > 0;
51595
51625
  }
51596
51626
  async function maybeAutoStartProxyForGui() {
51597
- if (guiProxyAutoStartDone) return;
51598
51627
  const store2 = useStore.getState();
51599
51628
  const status = store2.proxyState.status;
51629
+ if (privilegedWorkPending && !privilegedRepairAttempted) {
51630
+ console.log(
51631
+ `[Proxy] owner-ready: status=${status} repairAttempted=${privilegedRepairAttempted} needsRepair=${!!store2.proxyState.needsPrivilegedRepair} pending=${JSON.stringify(privilegedWorkPending)}`
51632
+ );
51633
+ }
51634
+ if (status === "running" && !privilegedRepairAttempted && shouldFinishPrivilegedSetup(store2.proxyState, lastPrivilegedError, privilegedWorkPending)) {
51635
+ privilegedRepairAttempted = true;
51636
+ await retryPrivilegedSetupOnce("proxy is running but its privileged setup never ran (no GUI owner at boot)");
51637
+ return;
51638
+ }
51639
+ if (guiProxyAutoStartDone) return;
51600
51640
  if (status === "running" || status === "starting") {
51601
51641
  guiProxyAutoStartDone = true;
51602
- if (status === "running" && shouldRetryPrivilegedSetup(store2.proxyState, lastPrivilegedError)) {
51603
- await retryPrivilegedSetupOnce("proxy is running but its privileged setup never ran (no GUI owner at boot)");
51604
- }
51605
51642
  return;
51606
51643
  }
51607
51644
  if (!store2.settings.autoStart) return;
@@ -51632,7 +51669,7 @@ async function retryPrivilegedSetupOnce(reason) {
51632
51669
  }
51633
51670
  async function handleStartProxy() {
51634
51671
  const alreadyRunning = isCaddyRunning();
51635
- const repairPending = !!useStore.getState().proxyState.needsPrivilegedRepair;
51672
+ const repairPending = !!useStore.getState().proxyState.needsPrivilegedRepair || !!privilegedWorkPending;
51636
51673
  if (alreadyRunning && !repairPending) {
51637
51674
  const { probeProxyReachability: probeProxyReachability2 } = await __vitePreload(async () => {
51638
51675
  const { probeProxyReachability: probeProxyReachability3 } = await Promise.resolve().then(() => dnsPlatform);
@@ -51762,11 +51799,13 @@ async function handleStartProxy() {
51762
51799
  });
51763
51800
  if (!sudoResult.success) {
51764
51801
  privilegedFailure = sudoResult.error || "unknown error";
51802
+ privilegedWorkPending = privilegedFailure;
51765
51803
  console.error(
51766
51804
  `[Proxy] privileged setup failed (continuing degraded — Caddy still starts): ${privilegedFailure}`
51767
51805
  );
51768
51806
  } else {
51769
51807
  console.log("[Proxy] sudo setup result: success");
51808
+ privilegedWorkPending = null;
51770
51809
  const resolverAudit = await auditResolverState().catch(() => null);
51771
51810
  if (resolverAudit && resolverAudit.in_sync === false && resolverAudit.missing.length > 0) {
51772
51811
  privilegedFailure = `DNS resolver files missing after privileged setup: ${resolverAudit.missing.join(", ")}`;
@@ -51775,6 +51814,7 @@ async function handleStartProxy() {
51775
51814
  }
51776
51815
  } catch (privErr) {
51777
51816
  privilegedFailure = privErr?.message || String(privErr);
51817
+ privilegedWorkPending = privilegedFailure;
51778
51818
  console.error(
51779
51819
  "[Proxy] privileged setup threw (continuing degraded — Caddy still starts):",
51780
51820
  privilegedFailure
@@ -54668,6 +54708,10 @@ async function init() {
54668
54708
  process.exit(0);
54669
54709
  }
54670
54710
  };
54711
+ try {
54712
+ process.send?.({ type: "host-bridge:owner?" });
54713
+ } catch {
54714
+ }
54671
54715
  process.on("SIGINT", () => cleanup());
54672
54716
  process.on("SIGTERM", () => cleanup());
54673
54717
  shutdownHandler = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supbuddy",
3
- "version": "3.1.18",
3
+ "version": "3.1.23",
4
4
  "description": "Run multiple Supabase projects at once on custom local domains with HTTPS. A headless CLI and daemon (proxy, DNS, Supabase/Compose lifecycle, MCP) for macOS and Linux.",
5
5
  "keywords": [
6
6
  "supabase",