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.
- package/dist/cli.js +64 -29
- package/dist/index.js +51 -16
- 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
|
-
|
|
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.
|
|
87
|
+
await fs.promises.copyFile(filePath, bakPath);
|
|
80
88
|
} catch {}
|
|
81
89
|
}
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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.
|
|
472
|
+
await fs.promises.copyFile(filePath, bakPath);
|
|
465
473
|
} catch {}
|
|
466
474
|
}
|
|
467
|
-
|
|
468
|
-
|
|
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
|
};
|