@synkro-sh/cli 1.4.60 → 1.4.61

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/bootstrap.js CHANGED
@@ -911,63 +911,93 @@ function decodeJwtExp(jwt: string): number {
911
911
  }
912
912
 
913
913
  export async function refreshJwt(jwt: string): Promise<string> {
914
- try {
915
- const creds = JSON.parse(readFileSync(CREDS_PATH, 'utf-8'));
916
- const rt = creds.refresh_token;
917
- if (!rt) return jwt;
914
+ const creds = JSON.parse(readFileSync(CREDS_PATH, 'utf-8'));
915
+ const rt = creds.refresh_token;
916
+ if (!rt) return jwt;
918
917
 
919
- const resp = await fetch(GATEWAY_URL + '/api/auth/refresh', {
920
- method: 'POST',
921
- headers: { 'Content-Type': 'application/json' },
922
- body: JSON.stringify({ refresh_token: rt }),
923
- signal: AbortSignal.timeout(4000),
924
- });
925
- const data = await resp.json() as any;
926
- const newAt = data.access_token;
927
- if (!newAt) return jwt;
928
-
929
- const newRt = data.refresh_token || rt;
930
- const existing = (() => {
931
- try { return JSON.parse(readFileSync(CREDS_PATH, 'utf-8')); } catch { return {}; }
932
- })();
933
- const updated = { ...existing, access_token: newAt, refresh_token: newRt };
934
- const tmp = CREDS_PATH + '.synkro.tmp';
935
- writeFileSync(tmp, JSON.stringify(updated, null, 2));
936
- renameSync(tmp, CREDS_PATH);
937
- return newAt;
938
- } catch {
918
+ const resp = await fetch(GATEWAY_URL + '/api/auth/refresh', {
919
+ method: 'POST',
920
+ headers: { 'Content-Type': 'application/json' },
921
+ body: JSON.stringify({ refresh_token: rt }),
922
+ signal: AbortSignal.timeout(4000),
923
+ });
924
+
925
+ if (!resp.ok) {
926
+ log('refresh failed: HTTP ' + resp.status);
939
927
  return jwt;
940
928
  }
929
+
930
+ const data = await resp.json() as any;
931
+ const newAt = data.access_token;
932
+ if (!newAt) return jwt;
933
+
934
+ const newRt = data.refresh_token || rt;
935
+ const existing = (() => {
936
+ try { return JSON.parse(readFileSync(CREDS_PATH, 'utf-8')); } catch { return {}; }
937
+ })();
938
+ const updated = { ...existing, access_token: newAt, refresh_token: newRt };
939
+ const tmp = CREDS_PATH + '.synkro.tmp';
940
+ writeFileSync(tmp, JSON.stringify(updated, null, 2));
941
+ renameSync(tmp, CREDS_PATH);
942
+ return newAt;
943
+ }
944
+
945
+ function jwtIsExpired(jwt: string): boolean {
946
+ const exp = decodeJwtExp(jwt);
947
+ return exp - Math.floor(Date.now() / 1000) < 60;
948
+ }
949
+
950
+ function lockIsStale(lockfile: string, maxAgeMs = 8000): boolean {
951
+ try {
952
+ const stat = require('node:fs').statSync(lockfile);
953
+ return Date.now() - stat.mtimeMs > maxAgeMs;
954
+ } catch {
955
+ return false;
956
+ }
941
957
  }
942
958
 
943
959
  export async function ensureFreshJwt(jwt: string): Promise<string> {
944
960
  if (!jwt) return jwt;
945
- const exp = decodeJwtExp(jwt);
946
- const now = Math.floor(Date.now() / 1000);
947
- if (exp - now >= 60) return jwt;
961
+ if (!jwtIsExpired(jwt)) return jwt;
948
962
 
949
963
  const lockfile = CREDS_PATH + '.lock';
964
+
965
+ // Clean stale lock left by a killed hook
966
+ if (existsSync(lockfile) && lockIsStale(lockfile)) {
967
+ try { unlinkSync(lockfile); } catch {}
968
+ }
969
+
950
970
  let fd = -1;
951
971
  try {
952
972
  fd = openSync(lockfile, FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_EXCL, 0o644);
953
973
  } catch {
954
- // Another process holds the lock \u2014 wait for it to finish and re-read
955
- for (let i = 0; i < 10; i++) {
956
- await new Promise(r => setTimeout(r, 300));
974
+ // Another process is refreshing \u2014 wait for it
975
+ for (let i = 0; i < 8; i++) {
976
+ await new Promise(r => setTimeout(r, 500));
957
977
  if (!existsSync(lockfile)) break;
958
978
  }
979
+ // Stale lock after full wait \u2014 force-remove
980
+ if (existsSync(lockfile) && lockIsStale(lockfile)) {
981
+ try { unlinkSync(lockfile); } catch {}
982
+ }
983
+ // Re-read \u2014 the winner should have written a fresh JWT
959
984
  const fresh = loadJwt();
960
- return fresh || jwt;
985
+ if (fresh && !jwtIsExpired(fresh)) return fresh;
986
+ // Winner's refresh failed \u2014 try to acquire lock ourselves
987
+ try {
988
+ fd = openSync(lockfile, FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_EXCL, 0o644);
989
+ } catch {
990
+ return fresh || jwt;
991
+ }
961
992
  }
962
993
 
963
994
  try {
964
- // Re-check \u2014 another hook may have just refreshed while we waited
995
+ // Re-check \u2014 another hook may have written fresh creds while we waited for lock
965
996
  const freshJwt = loadJwt();
966
- if (freshJwt) {
967
- const freshExp = decodeJwtExp(freshJwt);
968
- if (freshExp - Math.floor(Date.now() / 1000) >= 60) return freshJwt;
969
- }
997
+ if (freshJwt && !jwtIsExpired(freshJwt)) return freshJwt;
970
998
  return await refreshJwt(jwt);
999
+ } catch {
1000
+ return jwt;
971
1001
  } finally {
972
1002
  try { closeSync(fd); } catch {}
973
1003
  try { unlinkSync(lockfile); } catch {}
@@ -5137,7 +5167,7 @@ function writeConfigEnv(opts) {
5137
5167
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
5138
5168
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
5139
5169
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
5140
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.60")}`
5170
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.61")}`
5141
5171
  ];
5142
5172
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
5143
5173
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);