oc-auth-switcher 0.7.0 → 0.7.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.
Files changed (3) hide show
  1. package/dist/cli.js +64 -29
  2. package/dist/index.js +51 -16
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // src/accounts.ts
5
5
  import fs from "fs";
6
6
  import path2 from "path";
7
+ import { randomUUID } from "crypto";
7
8
 
8
9
  // src/constants.ts
9
10
  import path from "path";
@@ -69,18 +70,52 @@ function safeReadJSON(filePath, fallback) {
69
70
  }
70
71
  return fallback;
71
72
  }
72
- function safeWriteJSON(filePath, data) {
73
+ var writeQueues = new Map;
74
+ var MAX_RENAME_ATTEMPTS = 3;
75
+ var RENAME_RETRY_CODES = new Set(["ENOENT", "EEXIST", "EPERM"]);
76
+ function errorCode(error) {
77
+ return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
78
+ }
79
+ async function writeJSON(filePath, data, options) {
73
80
  ensureDir(filePath);
74
81
  const content = JSON.stringify(data, null, 2);
75
- const tmpPath = filePath + ".tmp";
76
82
  const bakPath = filePath + ".bak";
83
+ const rename = options.rename ?? fs.promises.rename;
84
+ const retryDelayMs = options.retryDelayMs ?? 5;
77
85
  if (fs.existsSync(filePath)) {
78
86
  try {
79
- fs.copyFileSync(filePath, bakPath);
87
+ await fs.promises.copyFile(filePath, bakPath);
80
88
  } catch {}
81
89
  }
82
- fs.writeFileSync(tmpPath, content, { mode: 384 });
83
- fs.renameSync(tmpPath, filePath);
90
+ let lastError;
91
+ for (let attempt = 1;attempt <= MAX_RENAME_ATTEMPTS; attempt++) {
92
+ const tmpPath = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
93
+ try {
94
+ await fs.promises.writeFile(tmpPath, content, { mode: 384 });
95
+ await rename(tmpPath, filePath);
96
+ return;
97
+ } catch (error) {
98
+ lastError = error;
99
+ await fs.promises.rm(tmpPath, { force: true }).catch(() => {});
100
+ if (!RENAME_RETRY_CODES.has(errorCode(error) ?? "") || attempt === MAX_RENAME_ATTEMPTS) {
101
+ break;
102
+ }
103
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
104
+ }
105
+ }
106
+ throw lastError;
107
+ }
108
+ function safeWriteJSON(filePath, data, options = {}) {
109
+ const previous = writeQueues.get(filePath) ?? Promise.resolve();
110
+ const current = previous.catch(() => {}).then(() => writeJSON(filePath, data, options)).catch((error) => {
111
+ console.warn(`[oc-auth-switcher] Could not safely write ${path2.basename(filePath)}:`, error instanceof Error ? error.message : String(error));
112
+ });
113
+ writeQueues.set(filePath, current);
114
+ current.finally(() => {
115
+ if (writeQueues.get(filePath) === current)
116
+ writeQueues.delete(filePath);
117
+ });
118
+ return current;
84
119
  }
85
120
  function loadAccounts() {
86
121
  const raw = safeReadJSON(ACCOUNTS_FILE, {
@@ -90,9 +125,9 @@ function loadAccounts() {
90
125
  return { accounts };
91
126
  }
92
127
  function saveAccounts(data) {
93
- safeWriteJSON(ACCOUNTS_FILE, data);
128
+ return safeWriteJSON(ACCOUNTS_FILE, data);
94
129
  }
95
- function addAccount(account) {
130
+ async function addAccount(account) {
96
131
  const data = loadAccounts();
97
132
  const idx = data.accounts.findIndex((a) => a.name === account.name);
98
133
  if (idx >= 0) {
@@ -100,23 +135,23 @@ function addAccount(account) {
100
135
  } else {
101
136
  data.accounts.push(account);
102
137
  }
103
- saveAccounts(data);
138
+ await saveAccounts(data);
104
139
  return data;
105
140
  }
106
- function removeAccount(name) {
141
+ async function removeAccount(name) {
107
142
  const data = loadAccounts();
108
143
  data.accounts = data.accounts.filter((a) => a.name !== name);
109
- saveAccounts(data);
144
+ await saveAccounts(data);
110
145
  return data;
111
146
  }
112
- function updateAccountTokens(name, access, refresh, expires) {
147
+ async function updateAccountTokens(name, access, refresh, expires) {
113
148
  const data = loadAccounts();
114
149
  const account = data.accounts.find((a) => a.name === name);
115
150
  if (account) {
116
151
  account.access = access;
117
152
  account.refresh = refresh;
118
153
  account.expires = expires;
119
- saveAccounts(data);
154
+ await saveAccounts(data);
120
155
  }
121
156
  }
122
157
  async function refreshAccountToken(refreshTokenValue) {
@@ -207,7 +242,7 @@ function loadState() {
207
242
  return normalizeState(safeReadJSON(STATE_FILE, {}));
208
243
  }
209
244
  function saveState(state) {
210
- safeWriteJSON(STATE_FILE, state);
245
+ return safeWriteJSON(STATE_FILE, state);
211
246
  }
212
247
  function getThresholds(config) {
213
248
  if (typeof config.threshold === "number") {
@@ -720,7 +755,7 @@ ${BOLD}Starting OAuth flow for account: ${CYAN}${name}${RESET}
720
755
  ${RED}Authentication failed${RESET}`);
721
756
  process.exit(1);
722
757
  }
723
- addAccount({
758
+ await addAccount({
724
759
  name,
725
760
  access: exchangeResult.access,
726
761
  refresh: exchangeResult.refresh,
@@ -733,7 +768,7 @@ ${GREEN}Account "${name}" added successfully.${RESET}`);
733
768
  if (data.accounts.length === 1) {
734
769
  const state = loadState();
735
770
  state.currentAccount = name;
736
- saveState(state);
771
+ await saveState(state);
737
772
  console.log(`
738
773
  ${YELLOW}This is the only account \u2014 set as active.${RESET}`);
739
774
  }
@@ -782,10 +817,10 @@ ${RED}Re-authentication failed${RESET}`);
782
817
  account.access = exchangeResult.access;
783
818
  account.refresh = exchangeResult.refresh;
784
819
  account.expires = exchangeResult.expires;
785
- saveAccounts(data);
820
+ await saveAccounts(data);
786
821
  const state = loadState();
787
822
  clearAuthFailure(state, name);
788
- saveState(state);
823
+ await saveState(state);
789
824
  console.log(`
790
825
  ${GREEN}Account "${name}" re-authenticated successfully.${RESET}`);
791
826
  }
@@ -798,7 +833,7 @@ async function refreshExpiredAccounts(data, state) {
798
833
  account.access = result.access;
799
834
  account.refresh = result.refresh;
800
835
  account.expires = result.expires;
801
- updateAccountTokens(account.name, result.access, result.refresh, result.expires);
836
+ await updateAccountTokens(account.name, result.access, result.refresh, result.expires);
802
837
  clearAuthFailure(state, account.name);
803
838
  }
804
839
  }
@@ -809,7 +844,7 @@ async function cmdUsage(args) {
809
844
  const data = loadAccounts();
810
845
  const state = loadState();
811
846
  await refreshExpiredAccounts(data, state);
812
- saveState(state);
847
+ await saveState(state);
813
848
  resolveStaleMetrics(state);
814
849
  ensureAccountsInState(state, data.accounts.map((a) => a.name));
815
850
  const thresholds = getThresholds(state.config);
@@ -871,7 +906,7 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
871
906
  new Promise(() => {});
872
907
  }
873
908
  }
874
- function cmdConfig(args) {
909
+ async function cmdConfig(args) {
875
910
  const state = loadState();
876
911
  if (args.length === 0) {
877
912
  const thresholds = getThresholds(state.config);
@@ -921,9 +956,9 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
921
956
  console.log(`${GREEN}Reset to default threshold: ${(DEFAULT_THRESHOLD * 100).toFixed(0)}%${RESET}`);
922
957
  }
923
958
  }
924
- saveState(state);
959
+ await saveState(state);
925
960
  }
926
- function cmdSwitch(args) {
961
+ async function cmdSwitch(args) {
927
962
  const data = loadAccounts();
928
963
  if (data.accounts.length === 0) {
929
964
  console.error(`${RED}No accounts configured.${RESET}`);
@@ -950,7 +985,7 @@ ${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
950
985
  console.log(`${CYAN}Switching to account: ${name}...${RESET}`);
951
986
  const state = loadState();
952
987
  state.currentAccount = name;
953
- saveState(state);
988
+ await saveState(state);
954
989
  console.log(`${GREEN}Switched to "${name}". Will take effect on the next API request.${RESET}`);
955
990
  }
956
991
  function healthLabel(health) {
@@ -1044,7 +1079,7 @@ ${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
1044
1079
  console.log(` ${DIM}State file: ${STATE_FILE}${RESET}`);
1045
1080
  console.log();
1046
1081
  }
1047
- function cmdRemove(args) {
1082
+ async function cmdRemove(args) {
1048
1083
  const name = args[0];
1049
1084
  if (!name) {
1050
1085
  console.error(`${RED}Usage: oc-auth-switcher remove <account-name>${RESET}`);
@@ -1056,12 +1091,12 @@ function cmdRemove(args) {
1056
1091
  console.error(`${RED}Account "${name}" not found${RESET}`);
1057
1092
  process.exit(1);
1058
1093
  }
1059
- removeAccount(name);
1094
+ await removeAccount(name);
1060
1095
  console.log(`${GREEN}Account "${name}" removed.${RESET}`);
1061
1096
  const state = loadState();
1062
1097
  if (state.currentAccount === name) {
1063
1098
  state.currentAccount = null;
1064
- saveState(state);
1099
+ await saveState(state);
1065
1100
  console.log(`${YELLOW}This was the active account. Rotation will pick a new one automatically.${RESET}`);
1066
1101
  }
1067
1102
  }
@@ -1112,18 +1147,18 @@ async function main() {
1112
1147
  break;
1113
1148
  case "config":
1114
1149
  case "c":
1115
- cmdConfig(commandArgs);
1150
+ await cmdConfig(commandArgs);
1116
1151
  break;
1117
1152
  case "switch":
1118
1153
  case "s":
1119
- cmdSwitch(commandArgs);
1154
+ await cmdSwitch(commandArgs);
1120
1155
  break;
1121
1156
  case "status":
1122
1157
  cmdStatus();
1123
1158
  break;
1124
1159
  case "remove":
1125
1160
  case "rm":
1126
- cmdRemove(commandArgs);
1161
+ await cmdRemove(commandArgs);
1127
1162
  break;
1128
1163
  case "help":
1129
1164
  case "--help":
package/dist/index.js CHANGED
@@ -406,6 +406,7 @@ async function exchange(input, verifier, redirectUri, expectedState) {
406
406
  // src/accounts.ts
407
407
  import fs from "node:fs";
408
408
  import path2 from "node:path";
409
+ import { randomUUID } from "node:crypto";
409
410
 
410
411
  // src/constants.ts
411
412
  import path from "node:path";
@@ -454,18 +455,52 @@ function safeReadJSON(filePath, fallback) {
454
455
  }
455
456
  return fallback;
456
457
  }
457
- function safeWriteJSON(filePath, data) {
458
+ var writeQueues = new Map;
459
+ var MAX_RENAME_ATTEMPTS = 3;
460
+ var RENAME_RETRY_CODES = new Set(["ENOENT", "EEXIST", "EPERM"]);
461
+ function errorCode(error) {
462
+ return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
463
+ }
464
+ async function writeJSON(filePath, data, options) {
458
465
  ensureDir(filePath);
459
466
  const content = JSON.stringify(data, null, 2);
460
- const tmpPath = filePath + ".tmp";
461
467
  const bakPath = filePath + ".bak";
468
+ const rename = options.rename ?? fs.promises.rename;
469
+ const retryDelayMs = options.retryDelayMs ?? 5;
462
470
  if (fs.existsSync(filePath)) {
463
471
  try {
464
- fs.copyFileSync(filePath, bakPath);
472
+ await fs.promises.copyFile(filePath, bakPath);
465
473
  } catch {}
466
474
  }
467
- fs.writeFileSync(tmpPath, content, { mode: 384 });
468
- fs.renameSync(tmpPath, filePath);
475
+ let lastError;
476
+ for (let attempt = 1;attempt <= MAX_RENAME_ATTEMPTS; attempt++) {
477
+ const tmpPath = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
478
+ try {
479
+ await fs.promises.writeFile(tmpPath, content, { mode: 384 });
480
+ await rename(tmpPath, filePath);
481
+ return;
482
+ } catch (error) {
483
+ lastError = error;
484
+ await fs.promises.rm(tmpPath, { force: true }).catch(() => {});
485
+ if (!RENAME_RETRY_CODES.has(errorCode(error) ?? "") || attempt === MAX_RENAME_ATTEMPTS) {
486
+ break;
487
+ }
488
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
489
+ }
490
+ }
491
+ throw lastError;
492
+ }
493
+ function safeWriteJSON(filePath, data, options = {}) {
494
+ const previous = writeQueues.get(filePath) ?? Promise.resolve();
495
+ const current = previous.catch(() => {}).then(() => writeJSON(filePath, data, options)).catch((error) => {
496
+ console.warn(`[oc-auth-switcher] Could not safely write ${path2.basename(filePath)}:`, error instanceof Error ? error.message : String(error));
497
+ });
498
+ writeQueues.set(filePath, current);
499
+ current.finally(() => {
500
+ if (writeQueues.get(filePath) === current)
501
+ writeQueues.delete(filePath);
502
+ });
503
+ return current;
469
504
  }
470
505
  function loadAccounts() {
471
506
  const raw = safeReadJSON(ACCOUNTS_FILE, {
@@ -475,16 +510,16 @@ function loadAccounts() {
475
510
  return { accounts };
476
511
  }
477
512
  function saveAccounts(data) {
478
- safeWriteJSON(ACCOUNTS_FILE, data);
513
+ return safeWriteJSON(ACCOUNTS_FILE, data);
479
514
  }
480
- function updateAccountTokens(name, access, refresh, expires) {
515
+ async function updateAccountTokens(name, access, refresh, expires) {
481
516
  const data = loadAccounts();
482
517
  const account = data.accounts.find((a) => a.name === name);
483
518
  if (account) {
484
519
  account.access = access;
485
520
  account.refresh = refresh;
486
521
  account.expires = expires;
487
- saveAccounts(data);
522
+ await saveAccounts(data);
488
523
  }
489
524
  }
490
525
  async function refreshAccountToken(refreshTokenValue) {
@@ -575,7 +610,7 @@ function loadState() {
575
610
  return normalizeState(safeReadJSON(STATE_FILE, {}));
576
611
  }
577
612
  function saveState(state) {
578
- safeWriteJSON(STATE_FILE, state);
613
+ return safeWriteJSON(STATE_FILE, state);
579
614
  }
580
615
  function getThresholds(config) {
581
616
  if (typeof config.threshold === "number") {
@@ -865,13 +900,13 @@ function selectionSnapshot(state) {
865
900
  currentAccount: state.currentAccount
866
901
  };
867
902
  }
868
- function saveRequestState(state, initiallyLoaded) {
903
+ async function saveRequestState(state, initiallyLoaded) {
869
904
  const onDisk = loadState();
870
905
  const selectionChangedSinceLoad = onDisk.currentAccount !== initiallyLoaded.currentAccount;
871
906
  if (selectionChangedSinceLoad) {
872
907
  state.currentAccount = onDisk.currentAccount;
873
908
  }
874
- saveState(state);
909
+ await saveState(state);
875
910
  }
876
911
  function getRequestModel(body) {
877
912
  if (typeof body !== "string")
@@ -973,13 +1008,13 @@ var AuthSwitcherPlugin = async ({ client }) => {
973
1008
  account.access = result.access;
974
1009
  account.refresh = result.refresh;
975
1010
  account.expires = result.expires;
976
- updateAccountTokens(account.name, result.access, result.refresh, result.expires);
1011
+ await updateAccountTokens(account.name, result.access, result.refresh, result.expires);
977
1012
  } else {
978
1013
  markAuthFailure(state, account.name);
979
1014
  const available = accounts.filter((candidate) => !attemptedAccounts.has(candidate.name) && (!state.authFailures[candidate.name] || state.authFailures[candidate.name] <= Date.now()));
980
1015
  const next = available.length > 0 ? selectAccount(available, state, model).account : undefined;
981
1016
  if (!next) {
982
- saveRequestState(state, initiallyLoadedSelection);
1017
+ await saveRequestState(state, initiallyLoadedSelection);
983
1018
  throw new Error(`[oc-auth-switcher] All accounts failed token refresh`);
984
1019
  }
985
1020
  account = next;
@@ -1021,7 +1056,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
1021
1056
  next.access = result.access;
1022
1057
  next.refresh = result.refresh;
1023
1058
  next.expires = result.expires;
1024
- updateAccountTokens(next.name, result.access, result.refresh, result.expires);
1059
+ await updateAccountTokens(next.name, result.access, result.refresh, result.expires);
1025
1060
  }
1026
1061
  }
1027
1062
  const retryHeaders = mergeHeaders(input, init);
@@ -1036,7 +1071,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
1036
1071
  });
1037
1072
  updateUsageFromHeaders(state, next.name, retryResponse.headers);
1038
1073
  clearAuthFailure(state, next.name);
1039
- saveRequestState(state, initiallyLoadedSelection);
1074
+ await saveRequestState(state, initiallyLoadedSelection);
1040
1075
  return createStrippedStream(retryResponse);
1041
1076
  }
1042
1077
  }
@@ -1044,7 +1079,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
1044
1079
  updateUsageFromHeaders(state, account.name, response.headers);
1045
1080
  clearAuthFailure(state, account.name);
1046
1081
  state.requestCount = (state.requestCount || 0) + 1;
1047
- saveRequestState(state, initiallyLoadedSelection);
1082
+ await saveRequestState(state, initiallyLoadedSelection);
1048
1083
  return createStrippedStream(response);
1049
1084
  }
1050
1085
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-auth-switcher",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "OpenCode auth plugin for multi-account Anthropic Claude Max rotation with automatic failover.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",