orchestrating 0.4.0 → 0.4.1
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/bin/orch +87 -26
- package/package.json +1 -1
package/bin/orch
CHANGED
|
@@ -57,12 +57,17 @@ function clearAuth() {
|
|
|
57
57
|
const SUPABASE_URL = process.env.SUPABASE_URL || "https://vhyqfzdskgrmnxrdaqql.supabase.co";
|
|
58
58
|
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZoeXFmemRza2dybW54cmRhcXFsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE1NjU5NTYsImV4cCI6MjA4NzE0MTk1Nn0.hWAceFshHoeTlnwThM08enNWG-ZeXICl3uyi-AmIJEk";
|
|
59
59
|
|
|
60
|
+
// Returns one of:
|
|
61
|
+
// { ok: true, token } — refreshed successfully
|
|
62
|
+
// { ok: false, permanent: true } — refresh token invalid/revoked; re-auth required
|
|
63
|
+
// { ok: false, permanent: false } — transient failure (network/5xx); safe to retry with backoff
|
|
60
64
|
async function refreshAuthToken() {
|
|
61
65
|
const auth = loadStoredAuth();
|
|
62
|
-
if (!auth || !auth.refresh_token) return
|
|
66
|
+
if (!auth || !auth.refresh_token) return { ok: false, permanent: true };
|
|
63
67
|
|
|
68
|
+
let res;
|
|
64
69
|
try {
|
|
65
|
-
|
|
70
|
+
res = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
|
|
66
71
|
method: "POST",
|
|
67
72
|
headers: {
|
|
68
73
|
"Content-Type": "application/json",
|
|
@@ -70,20 +75,30 @@ async function refreshAuthToken() {
|
|
|
70
75
|
},
|
|
71
76
|
body: JSON.stringify({ refresh_token: auth.refresh_token }),
|
|
72
77
|
});
|
|
78
|
+
} catch {
|
|
79
|
+
// Network error / server unreachable — transient
|
|
80
|
+
return { ok: false, permanent: false };
|
|
81
|
+
}
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (data.access_token) {
|
|
83
|
+
if (res.ok) {
|
|
84
|
+
let data;
|
|
85
|
+
try { data = await res.json(); } catch { return { ok: false, permanent: false }; }
|
|
86
|
+
if (data && data.access_token) {
|
|
78
87
|
saveAuth({
|
|
79
88
|
access_token: data.access_token,
|
|
80
89
|
refresh_token: data.refresh_token || auth.refresh_token,
|
|
81
90
|
expires_at: data.expires_at || 0,
|
|
82
91
|
});
|
|
83
|
-
return data.access_token;
|
|
92
|
+
return { ok: true, token: data.access_token };
|
|
84
93
|
}
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
return { ok: false, permanent: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 400/401/403 → refresh token rejected (permanent). 408/429/5xx/522 → transient.
|
|
98
|
+
if (res.status === 400 || res.status === 401 || res.status === 403) {
|
|
99
|
+
return { ok: false, permanent: true };
|
|
100
|
+
}
|
|
101
|
+
return { ok: false, permanent: false };
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
function getAuthToken() {
|
|
@@ -480,12 +495,16 @@ async function handleDaemon(projectsDir) {
|
|
|
480
495
|
process.exit(1);
|
|
481
496
|
}
|
|
482
497
|
if (isTokenExpired()) {
|
|
483
|
-
const
|
|
484
|
-
if (
|
|
485
|
-
token =
|
|
486
|
-
} else {
|
|
498
|
+
const r = await refreshAuthToken();
|
|
499
|
+
if (r.ok) {
|
|
500
|
+
token = r.token;
|
|
501
|
+
} else if (r.permanent) {
|
|
487
502
|
console.error(`${RED}Session expired. Run 'orch login' to re-authenticate.${RESET}`);
|
|
488
503
|
process.exit(1);
|
|
504
|
+
} else {
|
|
505
|
+
// Auth server unreachable at startup — keep the (stale) token and let the
|
|
506
|
+
// reconnect loop retry with backoff rather than crash-loop under launchd.
|
|
507
|
+
console.error(`${RED}Could not reach auth server — will keep retrying in the background.${RESET}`);
|
|
489
508
|
}
|
|
490
509
|
}
|
|
491
510
|
|
|
@@ -497,11 +516,17 @@ async function handleDaemon(projectsDir) {
|
|
|
497
516
|
const PING_INTERVAL_MS = 30_000;
|
|
498
517
|
const PONG_TIMEOUT_MS = 10_000;
|
|
499
518
|
const HEARTBEAT_TIMEOUT_MS = 65_000; // expect server heartbeat every 30s, allow 2 missed + margin
|
|
519
|
+
const RECONNECT_BASE_SEC = 2; // initial reconnect delay
|
|
520
|
+
const RECONNECT_MAX_SEC = 300; // cap backoff at 5 min so a down server isn't hammered
|
|
521
|
+
const REAUTH_POLL_SEC = 300; // while paused for re-auth, re-check the token every 5 min
|
|
500
522
|
let ws = null;
|
|
501
523
|
let pingTimer = null;
|
|
502
524
|
let pongTimer = null;
|
|
503
525
|
let reconnectTimer = null;
|
|
504
526
|
let heartbeatTimer = null;
|
|
527
|
+
let reauthTimer = null;
|
|
528
|
+
let reconnectAttempts = 0; // grows on consecutive failures; reset on a successful connect
|
|
529
|
+
let needsReauth = false; // true once the refresh token is rejected; pauses the reconnect loop
|
|
505
530
|
|
|
506
531
|
// Scan project directories
|
|
507
532
|
const homeDir = os.homedir();
|
|
@@ -536,25 +561,59 @@ async function handleDaemon(projectsDir) {
|
|
|
536
561
|
}, HEARTBEAT_TIMEOUT_MS);
|
|
537
562
|
}
|
|
538
563
|
|
|
539
|
-
function scheduleReconnect(
|
|
540
|
-
if (reconnecting) return;
|
|
564
|
+
function scheduleReconnect(minSec) {
|
|
565
|
+
if (reconnecting || needsReauth) return;
|
|
541
566
|
reconnecting = true;
|
|
567
|
+
// Exponential backoff with full jitter, capped — bounds the request rate when the
|
|
568
|
+
// server or auth endpoint is down (a fixed delay meant relentless retries forever).
|
|
569
|
+
const exp = Math.min(RECONNECT_MAX_SEC, RECONNECT_BASE_SEC * 2 ** reconnectAttempts);
|
|
570
|
+
const ceil = Math.max(minSec, exp);
|
|
571
|
+
const delay = Math.round(ceil / 2 + Math.random() * (ceil / 2));
|
|
572
|
+
reconnectAttempts++;
|
|
542
573
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
543
574
|
reconnectTimer = setTimeout(() => {
|
|
544
575
|
reconnecting = false;
|
|
545
576
|
connect();
|
|
546
|
-
},
|
|
577
|
+
}, delay * 1000);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Refresh token was rejected. Stop reconnecting (no point hammering the server) and
|
|
581
|
+
// wait for the user to run `orch login`. Staying alive — rather than exiting — avoids a
|
|
582
|
+
// launchd/systemd KeepAlive respawn loop that would just re-trigger the request flood.
|
|
583
|
+
function enterReauthWait() {
|
|
584
|
+
if (needsReauth) return;
|
|
585
|
+
needsReauth = true;
|
|
586
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
587
|
+
process.stderr.write(`${RED}${PREFIX} Session invalid — run 'orch login' to re-authenticate.${RESET}\n`);
|
|
588
|
+
process.stderr.write(`${DIM}${PREFIX} Paused; will resume automatically once re-authenticated.${RESET}\n`);
|
|
589
|
+
const check = () => {
|
|
590
|
+
const auth = loadStoredAuth();
|
|
591
|
+
if (auth && auth.access_token && !isTokenExpired()) {
|
|
592
|
+
needsReauth = false;
|
|
593
|
+
reconnectAttempts = 0;
|
|
594
|
+
token = auth.access_token;
|
|
595
|
+
process.stderr.write(`${GREEN}${PREFIX} Re-authenticated — reconnecting${RESET}\n`);
|
|
596
|
+
connect();
|
|
597
|
+
} else {
|
|
598
|
+
reauthTimer = setTimeout(check, REAUTH_POLL_SEC * 1000);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
reauthTimer = setTimeout(check, REAUTH_POLL_SEC * 1000);
|
|
547
602
|
}
|
|
548
603
|
|
|
549
604
|
async function connect() {
|
|
605
|
+
if (needsReauth) return; // paused until the user re-authenticates
|
|
550
606
|
// Refresh token if needed before connecting
|
|
551
607
|
if (isTokenExpired()) {
|
|
552
|
-
const
|
|
553
|
-
if (
|
|
554
|
-
token =
|
|
608
|
+
const r = await refreshAuthToken();
|
|
609
|
+
if (r.ok) {
|
|
610
|
+
token = r.token;
|
|
555
611
|
process.stderr.write(`${DIM}${PREFIX} Token refreshed${RESET}\n`);
|
|
612
|
+
} else if (r.permanent) {
|
|
613
|
+
enterReauthWait();
|
|
614
|
+
return;
|
|
556
615
|
} else {
|
|
557
|
-
process.stderr.write(`${RED}${PREFIX} Token refresh failed —
|
|
616
|
+
process.stderr.write(`${RED}${PREFIX} Token refresh failed (auth server unreachable) — backing off${RESET}\n`);
|
|
558
617
|
scheduleReconnect(10);
|
|
559
618
|
return;
|
|
560
619
|
}
|
|
@@ -568,6 +627,7 @@ async function handleDaemon(projectsDir) {
|
|
|
568
627
|
ws = sock;
|
|
569
628
|
|
|
570
629
|
sock.on("open", () => {
|
|
630
|
+
reconnectAttempts = 0; // healthy connection — reset backoff
|
|
571
631
|
sock.send(JSON.stringify({
|
|
572
632
|
type: "register_daemon",
|
|
573
633
|
token,
|
|
@@ -610,14 +670,14 @@ async function handleDaemon(projectsDir) {
|
|
|
610
670
|
process.stderr.write(`${RED}${PREFIX} Server: ${msg.error}${RESET}\n`);
|
|
611
671
|
if (/unauthorized|auth|token/i.test(msg.error || "")) {
|
|
612
672
|
// Don't reconnect from here — let the close handler do it after refreshing
|
|
613
|
-
refreshAuthToken().then((
|
|
614
|
-
if (
|
|
615
|
-
token =
|
|
673
|
+
refreshAuthToken().then((r) => {
|
|
674
|
+
if (r.ok) {
|
|
675
|
+
token = r.token;
|
|
616
676
|
process.stderr.write(`${DIM}${PREFIX} Token refreshed — will reconnect${RESET}\n`);
|
|
617
|
-
} else {
|
|
618
|
-
|
|
619
|
-
process.exit(1);
|
|
677
|
+
} else if (r.permanent) {
|
|
678
|
+
enterReauthWait();
|
|
620
679
|
}
|
|
680
|
+
// transient: leave it — the close handler's backoff retry will try again
|
|
621
681
|
});
|
|
622
682
|
}
|
|
623
683
|
return;
|
|
@@ -769,6 +829,7 @@ async function handleDaemon(projectsDir) {
|
|
|
769
829
|
if (pongTimer) clearTimeout(pongTimer);
|
|
770
830
|
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
771
831
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
832
|
+
if (reauthTimer) clearTimeout(reauthTimer);
|
|
772
833
|
if (ws) ws.close();
|
|
773
834
|
process.exit(0);
|
|
774
835
|
};
|