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 +44 -16
- package/dist/daemon/worker.cjs +57 -13
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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) =>
|
|
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
|
-
|
|
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");
|
package/dist/daemon/worker.cjs
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
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.
|
|
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",
|