Skip to content

Commit e8c0227

Browse files
committed
Preserve auth-failure disable invariants
1 parent 7666837 commit e8c0227

4 files changed

Lines changed: 74 additions & 2 deletions

File tree

lib/accounts.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,12 @@ export class AccountManager {
953953
if (enabled && account.disabledReason === "auth-failure") {
954954
return { ok: false, reason: "auth-failure-blocked" };
955955
}
956+
// Once an account is auth-failure disabled, callers must refresh credentials
957+
// instead of downgrading the reason to a manually re-enableable state.
958+
if (!enabled && account.disabledReason === "auth-failure") {
959+
account.enabled = false;
960+
return { ok: true, account };
961+
}
956962
account.enabled = enabled;
957963
if (enabled) {
958964
delete account.disabledReason;
@@ -1065,6 +1071,8 @@ export class AccountManager {
10651071
while (true) {
10661072
flushIterations += 1;
10671073
if (flushIterations > MAX_FLUSH_ITERATIONS) {
1074+
// This is intentionally far above realistic debounce re-arm chains; if we
1075+
// still haven't converged, shutdown callers log the failure and continue exit.
10681076
log.warn("flushPendingSave exceeded max iterations; possible save loop", {
10691077
iterations: flushIterations - 1,
10701078
});

lib/storage/migrations.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,10 @@ export function migrateV1ToV3(v1: AccountStorageV1): AccountStorageV3 {
123123
accessToken: account.accessToken,
124124
expiresAt: account.expiresAt,
125125
enabled: normalizeStoredEnabled(account.enabled),
126-
disabledReason: account.enabled === false ? account.disabledReason ?? "user" : undefined,
126+
disabledReason:
127+
account.enabled === false
128+
? normalizeAccountDisabledReason(account.disabledReason) ?? "user"
129+
: undefined,
127130
addedAt: account.addedAt,
128131
lastUsed: account.lastUsed,
129132
lastSwitchReason: account.lastSwitchReason,

test/accounts.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,7 @@ describe("AccountManager", () => {
12401240
});
12411241
});
12421242

1243-
it("preserves the legacy setAccountEnabled account-or-null contract", () => {
1243+
it("ignores explicit disable-reason overrides for auth-failure disabled accounts", () => {
12441244
const now = Date.now();
12451245
const stored = {
12461246
version: 3 as const,
@@ -1258,14 +1258,50 @@ describe("AccountManager", () => {
12581258

12591259
const manager = new AccountManager(undefined, stored);
12601260

1261+
const disabled = manager.trySetAccountEnabled(0, false, "user");
1262+
expect(disabled).toMatchObject({
1263+
ok: true,
1264+
account: {
1265+
enabled: false,
1266+
disabledReason: "auth-failure",
1267+
},
1268+
});
1269+
1270+
expect(manager.trySetAccountEnabled(0, true)).toEqual({
1271+
ok: false,
1272+
reason: "auth-failure-blocked",
1273+
});
12611274
expect(manager.setAccountEnabled(0, true)).toBeNull();
1275+
});
1276+
1277+
it("preserves the legacy setAccountEnabled account-or-null contract", () => {
1278+
const now = Date.now();
1279+
const stored = {
1280+
version: 3 as const,
1281+
activeIndex: 0,
1282+
accounts: [
1283+
{
1284+
refreshToken: "token-1",
1285+
addedAt: now,
1286+
lastUsed: now,
1287+
},
1288+
],
1289+
};
1290+
1291+
const manager = new AccountManager(undefined, stored);
1292+
12621293
expect(manager.setAccountEnabled(99, false)).toBeNull();
12631294

12641295
const userDisabled = manager.setAccountEnabled(0, false, "user");
12651296
expect(userDisabled).toMatchObject({
12661297
enabled: false,
12671298
disabledReason: "user",
12681299
});
1300+
1301+
const reenabled = manager.setAccountEnabled(0, true);
1302+
expect(reenabled).toMatchObject({
1303+
enabled: true,
1304+
});
12691305
});
12701306
});
12711307

test/storage.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,31 @@ describe("storage", () => {
10741074
});
10751075
});
10761076

1077+
it("defaults invalid migrated legacy disabled reasons to user", () => {
1078+
const data = {
1079+
version: 1,
1080+
activeIndex: 0,
1081+
accounts: [
1082+
{
1083+
refreshToken: "t1",
1084+
accountId: "A",
1085+
enabled: false,
1086+
disabledReason: "future-reason",
1087+
addedAt: 1,
1088+
lastUsed: 1,
1089+
},
1090+
],
1091+
};
1092+
1093+
const result = normalizeAccountStorage(data);
1094+
expect(result?.version).toBe(3);
1095+
expect(result?.accounts[0]).toMatchObject({
1096+
refreshToken: "t1",
1097+
enabled: false,
1098+
disabledReason: "user",
1099+
});
1100+
});
1101+
10771102
it("omits enabled for enabled accounts during v1 migration", () => {
10781103
const data = {
10791104
version: 1,

0 commit comments

Comments
 (0)