diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03e96eed..e17f5731 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,12 +18,17 @@ all notable changes to this project. dates are ISO format (YYYY-MM-DD).
- **account storage schema**: V3 account metadata now includes optional `accountTags` and `accountNote`.
- **docs refresh for operational flows**: README + docs portal/development guides updated to reflect beginner commands, safe mode, interactive picker behavior, and backup/import safeguards.
- **test matrix expansion**: coverage now includes beginner UI helpers, safe-fix diagnostics edge cases, tag/note command behavior, and timestamped backup/import preview utilities.
+- **dependency security baseline**: refreshed lockfile dependency graph via `npm audit fix` to remove all known high/moderate advisories in the audited tree.
### fixed
- **non-interactive command guidance**: optional-index commands provide explicit usage guidance when interactive menus are unavailable.
- **doctor safe-fix edge path**: `codex-doctor fix` now reports a clear non-crashing message when no eligible account is available for auto-switch.
- **first-time import flow**: `codex-import` no longer fails with `No accounts to export` when storage is empty; pre-import backup is skipped cleanly in zero-account setups.
+- **oauth callback host alignment**: authorization redirect now uses `http://127.0.0.1:1455/auth/callback` to match the loopback server binding and avoid `localhost` resolver drift.
+- **oauth success-page resilience**: callback server now falls back to a built-in success HTML page when `oauth-success.html` is unavailable, preventing hard startup failure.
+- **oauth poll contract hardening**: `waitForCode(state)` now verifies the captured callback state before returning code, matching the declared interface contract.
+- **hybrid account selection eligibility**: token-bucket depletion is now enforced during hybrid selection/current-account reuse, preventing premature request failures when other accounts remain eligible.
## [5.4.0] - 2026-02-28
diff --git a/docs/README.md b/docs/README.md
index 9a1c586a..0584132d 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -18,6 +18,7 @@ Explore the engineering depth behind this plugin:
- **[Config Fields Guide](development/CONFIG_FIELDS.md)** - Understanding config keys, `id`, and `name`
- **[Testing Guide](development/TESTING.md)** - Test scenarios, verification procedures, integration testing
- **[TUI Parity Checklist](development/TUI_PARITY_CHECKLIST.md)** - Auth dashboard/UI parity requirements for future changes
+- **[Architecture Audit (2026-02-28)](development/ARCHITECTURE_AUDIT_2026-02-28.md)** - Full security/reliability audit findings and remediation summary
## Key Architectural Decisions
diff --git a/docs/development/ARCHITECTURE_AUDIT_2026-02-28.md b/docs/development/ARCHITECTURE_AUDIT_2026-02-28.md
new file mode 100644
index 00000000..b53054f8
--- /dev/null
+++ b/docs/development/ARCHITECTURE_AUDIT_2026-02-28.md
@@ -0,0 +1,48 @@
+# Architecture + Security Audit (2026-02-28)
+
+## Scope
+
+- Full repository audit across auth, request pipeline, account rotation, storage, and dependency supply chain.
+- Severity focus: Critical, High, Medium.
+- Remediation PR policy: fix-in-place for findings above threshold.
+
+## Findings and Remediations
+
+### 1) Dependency Vulnerabilities (High/Moderate)
+
+- Baseline `npm audit` reported 4 vulnerabilities (3 high, 1 moderate), including direct `hono` exposure plus transitive `rollup`, `minimatch`, and `ajv`.
+- Remediation: ran `npm audit fix`, updated lockfile graph, and verified `npm audit` reports zero vulnerabilities.
+
+### 2) OAuth Loopback Host Mismatch (Medium)
+
+- OAuth redirect URI used `localhost` while callback listener binds to `127.0.0.1`.
+- On environments where `localhost` resolves to non-IPv4 loopback, this can cause callback failures.
+- Remediation: aligned redirect URI to `http://127.0.0.1:1455/auth/callback`.
+
+### 3) Hybrid Selection vs Token-Bucket Eligibility Mismatch (Medium)
+
+- Hybrid account selection and current-account fast path did not enforce token availability.
+- This could pick accounts that are locally token-depleted and trigger avoidable request failure behavior.
+- Remediation:
+ - enforce token availability during current-account reuse and hybrid eligibility filtering;
+ - continue account traversal when local token consumption fails to avoid premature loop exit.
+
+### 4) OAuth Success-Page Single-Point Failure (Medium)
+
+- OAuth callback server loaded `oauth-success.html` synchronously at module import with no fallback.
+- If that asset was missing in a runtime package edge case, plugin startup could fail before auth flow execution.
+- Remediation:
+ - add resilient loader with warning telemetry;
+ - serve a built-in minimal success page when file load fails.
+ - enforce `waitForCode(state)` contract by checking captured callback state before returning a code.
+
+## Verification
+
+- `npm run lint` pass
+- `npm run typecheck` pass
+- `npm test` pass
+- `npm audit` reports zero vulnerabilities
+
+## Notes
+
+- This audit focused on root-cause correctness and supply-chain risk reduction, while preserving existing plugin APIs and storage format compatibility.
diff --git a/index.ts b/index.ts
index 4f7a4a59..1e5b7eb7 100644
--- a/index.ts
+++ b/index.ts
@@ -388,9 +388,9 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
validate: (input: string): string | undefined => {
const parsed = parseAuthorizationInput(input);
- if (!parsed.code) {
- return "No authorization code found. Paste the full callback URL (e.g., http://localhost:1455/auth/callback?code=...)";
- }
+ if (!parsed.code) {
+ return "No authorization code found. Paste the full callback URL (e.g., http://127.0.0.1:1455/auth/callback?code=...)";
+ }
if (!parsed.state) {
return "Missing OAuth state. Paste the full callback URL including both code and state parameters.";
}
@@ -2336,7 +2336,7 @@ while (attempted.size < Math.max(1, accountCount)) {
logWarn(
`Skipping account ${account.index + 1}: local token bucket depleted for ${modelFamily}${model ? `:${model}` : ""}`,
);
- break;
+ continue;
}
while (true) {
diff --git a/lib/accounts.ts b/lib/accounts.ts
index c53804c4..33d306b9 100644
--- a/lib/accounts.ts
+++ b/lib/accounts.ts
@@ -571,6 +571,9 @@ export class AccountManager {
getCurrentOrNextForFamilyHybrid(family: ModelFamily, model?: string | null, options?: HybridSelectionOptions): ManagedAccount | null {
const count = this.accounts.length;
if (count === 0) return null;
+ const quotaKey = model ? `${family}:${model}` : family;
+ const healthTracker = getHealthTracker();
+ const tokenTracker = getTokenTracker();
const currentIndex = this.currentAccountIndexByFamily[family];
if (currentIndex >= 0 && currentIndex < count) {
@@ -582,7 +585,8 @@ export class AccountManager {
clearExpiredRateLimits(currentAccount);
if (
!isRateLimitedForFamily(currentAccount, family, model) &&
- !this.isAccountCoolingDown(currentAccount)
+ !this.isAccountCoolingDown(currentAccount) &&
+ tokenTracker.getTokens(currentAccount.index, quotaKey) >= 1
) {
currentAccount.lastUsed = nowMs();
return currentAccount;
@@ -591,17 +595,16 @@ export class AccountManager {
}
}
- const quotaKey = model ? `${family}:${model}` : family;
- const healthTracker = getHealthTracker();
- const tokenTracker = getTokenTracker();
-
const accountsWithMetrics: AccountWithMetrics[] = this.accounts
.map((account): AccountWithMetrics | null => {
if (!account) return null;
if (account.enabled === false) return null;
clearExpiredRateLimits(account);
+ const tokensAvailable = tokenTracker.getTokens(account.index, quotaKey);
const isAvailable =
- !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
+ !isRateLimitedForFamily(account, family, model) &&
+ !this.isAccountCoolingDown(account) &&
+ tokensAvailable >= 1;
return {
index: account.index,
isAvailable,
diff --git a/lib/auth/auth.ts b/lib/auth/auth.ts
index 545d6365..45f35848 100644
--- a/lib/auth/auth.ts
+++ b/lib/auth/auth.ts
@@ -8,7 +8,7 @@ import { safeParseOAuthTokenResponse } from "../schemas.js";
export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
export const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
export const TOKEN_URL = "https://auth.openai.com/oauth/token";
-export const REDIRECT_URI = "http://localhost:1455/auth/callback";
+export const REDIRECT_URI = "http://127.0.0.1:1455/auth/callback";
export const SCOPE = "openid profile email offline_access";
/**
diff --git a/lib/auth/server.ts b/lib/auth/server.ts
index 1f83a105..731b45b3 100644
--- a/lib/auth/server.ts
+++ b/lib/auth/server.ts
@@ -7,7 +7,32 @@ import { logError, logWarn } from "../logger.js";
// Resolve path to oauth-success.html (one level up from auth/ subfolder)
const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const successHtml = fs.readFileSync(path.join(__dirname, "..", "oauth-success.html"), "utf-8");
+const SUCCESS_HTML_PATH = path.join(__dirname, "..", "oauth-success.html");
+const FALLBACK_SUCCESS_HTML = `
+
+
+
+ Authorization Complete
+
+
+ Authorization complete
+ You can return to OpenCode.
+
+`;
+
+function loadSuccessHtml(): string {
+ try {
+ return fs.readFileSync(SUCCESS_HTML_PATH, "utf-8");
+ } catch (error) {
+ logWarn("oauth-success.html missing; using fallback success page", {
+ path: SUCCESS_HTML_PATH,
+ error: (error as Error)?.message ?? String(error),
+ });
+ return FALLBACK_SUCCESS_HTML;
+ }
+}
+
+const successHtml = loadSuccessHtml();
/**
* Start a small local HTTP server that waits for /auth/callback and returns the code
@@ -41,7 +66,9 @@ export function startLocalOAuthServer({ state }: { state: string }): Promise {
+ waitForCode: async (expectedState: string) => {
const POLL_INTERVAL_MS = 100;
const TIMEOUT_MS = 5 * 60 * 1000;
const maxIterations = Math.floor(TIMEOUT_MS / POLL_INTERVAL_MS);
const poll = () => new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
for (let i = 0; i < maxIterations; i++) {
if (pollAborted) return null;
- const lastCode = (server as http.Server & { _lastCode?: string })._lastCode;
- if (lastCode) return { code: lastCode };
+ const codeStore = server as http.Server & { _lastCode?: string; _lastState?: string };
+ const lastCode = codeStore._lastCode;
+ const lastState = codeStore._lastState;
+ if (lastCode && lastState === expectedState) return { code: lastCode };
await poll();
}
logWarn("OAuth poll timeout after 5 minutes");
diff --git a/package-lock.json b/package-lock.json
index 0b6d3dff..3159b627 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -845,9 +845,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz",
- "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -859,9 +859,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz",
- "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -873,9 +873,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz",
- "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -887,9 +887,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz",
- "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -901,9 +901,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz",
- "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -915,9 +915,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz",
- "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -929,9 +929,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz",
- "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -943,9 +943,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz",
- "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -957,9 +957,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz",
- "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -971,9 +971,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz",
- "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -985,9 +985,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz",
- "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
@@ -999,9 +999,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz",
- "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -1013,9 +1013,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz",
- "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
@@ -1027,9 +1027,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz",
- "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -1041,9 +1041,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz",
- "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -1055,9 +1055,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz",
- "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -1069,9 +1069,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz",
- "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -1083,9 +1083,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz",
- "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -1097,9 +1097,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz",
- "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -1111,9 +1111,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz",
- "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
@@ -1125,9 +1125,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz",
- "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -1139,9 +1139,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz",
- "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -1153,9 +1153,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz",
- "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -1167,9 +1167,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz",
- "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -1181,9 +1181,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz",
- "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -1445,13 +1445,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -1710,9 +1710,9 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2395,9 +2395,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz",
- "integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==",
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz",
+ "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -2747,9 +2747,9 @@
}
},
"node_modules/minimatch": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
- "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -3046,9 +3046,9 @@
"license": "MIT"
},
"node_modules/rollup": {
- "version": "4.56.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
- "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3062,31 +3062,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.56.0",
- "@rollup/rollup-android-arm64": "4.56.0",
- "@rollup/rollup-darwin-arm64": "4.56.0",
- "@rollup/rollup-darwin-x64": "4.56.0",
- "@rollup/rollup-freebsd-arm64": "4.56.0",
- "@rollup/rollup-freebsd-x64": "4.56.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.56.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.56.0",
- "@rollup/rollup-linux-arm64-gnu": "4.56.0",
- "@rollup/rollup-linux-arm64-musl": "4.56.0",
- "@rollup/rollup-linux-loong64-gnu": "4.56.0",
- "@rollup/rollup-linux-loong64-musl": "4.56.0",
- "@rollup/rollup-linux-ppc64-gnu": "4.56.0",
- "@rollup/rollup-linux-ppc64-musl": "4.56.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.56.0",
- "@rollup/rollup-linux-riscv64-musl": "4.56.0",
- "@rollup/rollup-linux-s390x-gnu": "4.56.0",
- "@rollup/rollup-linux-x64-gnu": "4.56.0",
- "@rollup/rollup-linux-x64-musl": "4.56.0",
- "@rollup/rollup-openbsd-x64": "4.56.0",
- "@rollup/rollup-openharmony-arm64": "4.56.0",
- "@rollup/rollup-win32-arm64-msvc": "4.56.0",
- "@rollup/rollup-win32-ia32-msvc": "4.56.0",
- "@rollup/rollup-win32-x64-gnu": "4.56.0",
- "@rollup/rollup-win32-x64-msvc": "4.56.0",
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
diff --git a/test/accounts.test.ts b/test/accounts.test.ts
index 0d11a641..9626edc4 100644
--- a/test/accounts.test.ts
+++ b/test/accounts.test.ts
@@ -1785,6 +1785,29 @@ describe("AccountManager", () => {
expect(selected?.index).toBe(1);
});
+ it("skips token-depleted current account and selects account with available tokens", () => {
+ const now = Date.now();
+ const stored = {
+ version: 3 as const,
+ activeIndex: 0,
+ activeIndexByFamily: { codex: 0 },
+ accounts: [
+ { refreshToken: "token-1", addedAt: now, lastUsed: now },
+ { refreshToken: "token-2", addedAt: now, lastUsed: now - 10000 },
+ ],
+ };
+
+ const manager = new AccountManager(undefined, stored as never);
+ manager.setActiveIndex(0);
+ getTokenTracker().drain(0, "codex", 100);
+
+ const selected = manager.getCurrentOrNextForFamilyHybrid("codex");
+
+ expect(selected).not.toBeNull();
+ expect(selected?.refreshToken).toBe("token-2");
+ expect(selected?.index).toBe(1);
+ });
+
it("updates cursor and family index after hybrid selection", () => {
const now = Date.now();
const stored = {
diff --git a/test/auth.test.ts b/test/auth.test.ts
index 3f8b1005..2875a9aa 100644
--- a/test/auth.test.ts
+++ b/test/auth.test.ts
@@ -178,6 +178,10 @@ describe('Auth Module', () => {
});
describe('createAuthorizationFlow', () => {
+ it('uses loopback IPv4 redirect URI to match local callback binding', () => {
+ expect(REDIRECT_URI).toBe('http://127.0.0.1:1455/auth/callback');
+ });
+
it('should create authorization flow with PKCE', async () => {
const flow = await createAuthorizationFlow();
diff --git a/test/server-fallback.test.ts b/test/server-fallback.test.ts
new file mode 100644
index 00000000..7db53511
--- /dev/null
+++ b/test/server-fallback.test.ts
@@ -0,0 +1,74 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import type { IncomingMessage, ServerResponse } from "node:http";
+import { EventEmitter } from "node:events";
+
+describe("OAuth server success-page fallback", () => {
+ beforeEach(() => {
+ vi.resetModules();
+ vi.clearAllMocks();
+ });
+
+ it("uses fallback HTML when oauth-success.html is missing", async () => {
+ type MockServer = {
+ _handler?: (req: IncomingMessage, res: ServerResponse) => void;
+ listen: (
+ port: number,
+ host: string,
+ callback: () => void,
+ ) => MockServer;
+ close: () => void;
+ unref: () => void;
+ on: (event: string, handler: (err: NodeJS.ErrnoException) => void) => MockServer;
+ };
+
+ const mockServer: MockServer = {
+ _handler: undefined,
+ listen: (_port, _host, callback) => {
+ callback();
+ return mockServer;
+ },
+ close: () => {},
+ unref: () => {},
+ on: () => mockServer,
+ };
+
+ const createServer = vi.fn(
+ (handler: (req: IncomingMessage, res: ServerResponse) => void) => {
+ mockServer._handler = handler;
+ return mockServer;
+ },
+ );
+ const readFileSync = vi.fn(() => {
+ throw new Error("ENOENT");
+ });
+ const logWarn = vi.fn();
+ const logError = vi.fn();
+
+ vi.doMock("node:http", () => ({ default: { createServer } }));
+ vi.doMock("node:fs", () => ({ default: { readFileSync } }));
+ vi.doMock("../lib/logger.js", () => ({ logWarn, logError }));
+
+ const { startLocalOAuthServer } = await import("../lib/auth/server.js");
+ const serverInfo = await startLocalOAuthServer({ state: "state-1" });
+
+ expect(serverInfo.ready).toBe(true);
+ expect(logWarn).toHaveBeenCalledWith(
+ "oauth-success.html missing; using fallback success page",
+ expect.objectContaining({ error: "ENOENT" }),
+ );
+
+ const req = new EventEmitter() as IncomingMessage;
+ req.url = "/auth/callback?code=test-code&state=state-1";
+ const body = { value: "" };
+ const res = {
+ statusCode: 0,
+ setHeader: vi.fn(),
+ end: vi.fn((payload?: string) => {
+ body.value = payload ?? "";
+ }),
+ } as unknown as ServerResponse;
+
+ mockServer._handler?.(req, res);
+ expect(body.value).toContain("Authorization complete");
+ });
+});
diff --git a/test/server.unit.test.ts b/test/server.unit.test.ts
index ec6fb6a4..5d8a7da9 100644
--- a/test/server.unit.test.ts
+++ b/test/server.unit.test.ts
@@ -14,6 +14,7 @@ vi.mock('node:http', () => {
unref: vi.fn(),
on: vi.fn(),
_lastCode: undefined as string | undefined,
+ _lastState: undefined as string | undefined,
};
return {
@@ -46,12 +47,14 @@ describe('OAuth Server Unit Tests', () => {
let mockServer: ReturnType & {
_handler?: (req: IncomingMessage, res: ServerResponse) => void;
_lastCode?: string;
+ _lastState?: string;
};
beforeEach(() => {
vi.clearAllMocks();
mockServer = http.createServer(() => {}) as typeof mockServer;
mockServer._lastCode = undefined;
+ mockServer._lastState = undefined;
});
afterEach(() => {
@@ -190,6 +193,7 @@ describe('OAuth Server Unit Tests', () => {
requestHandler(req, res);
expect(mockServer._lastCode).toBe('captured-code');
+ expect(mockServer._lastState).toBe('test-state');
});
it('should handle request handler errors gracefully', () => {
@@ -280,6 +284,7 @@ describe('OAuth Server Unit Tests', () => {
const result = await startLocalOAuthServer({ state: 'test-state' });
mockServer._lastCode = 'the-code';
+ mockServer._lastState = 'test-state';
const code = await result.waitForCode('test-state');
expect(code).toEqual({ code: 'the-code' });
diff --git a/test/storage.test.ts b/test/storage.test.ts
index dedd7733..2f3df184 100644
--- a/test/storage.test.ts
+++ b/test/storage.test.ts
@@ -21,13 +21,9 @@ import {
previewImportAccounts,
createTimestampedBackupPath,
withAccountStorageTransaction,
+ type AccountStorageV3,
} from "../lib/storage.js";
-// Mocking the behavior we're about to implement for TDD
-// Since the functions aren't in lib/storage.ts yet, we'll need to mock them or
-// accept that this test won't even compile/run until we add them.
-// But Task 0 says: "Tests should fail initially (RED phase)"
-
describe("storage", () => {
describe("deduplication", () => {
it("remaps activeIndex after deduplication using active account key", () => {
@@ -108,56 +104,43 @@ describe("storage", () => {
});
it("should export accounts to a file", async () => {
- // @ts-ignore - exportAccounts doesn't exist yet
- const { exportAccounts } = await import("../lib/storage.js");
-
- const storage = {
+ const storage: AccountStorageV3 = {
version: 3,
activeIndex: 0,
- accounts: [{ accountId: "test", refreshToken: "ref", addedAt: 1, lastUsed: 2 }]
+ accounts: [{ accountId: "test", refreshToken: "ref", addedAt: 1, lastUsed: 2 }],
};
- // @ts-ignore
await saveAccounts(storage);
-
- // @ts-ignore
+
await exportAccounts(exportPath);
-
+
expect(existsSync(exportPath)).toBe(true);
const exported = JSON.parse(await fs.readFile(exportPath, "utf-8"));
expect(exported.accounts[0].accountId).toBe("test");
});
it("should fail export if file exists and force is false", async () => {
- // @ts-ignore
- const { exportAccounts } = await import("../lib/storage.js");
await fs.writeFile(exportPath, "exists");
-
- // @ts-ignore
+
await expect(exportAccounts(exportPath, false)).rejects.toThrow(/already exists/);
});
it("should import accounts from a file and merge", async () => {
- // @ts-ignore
- const { importAccounts } = await import("../lib/storage.js");
-
- const existing = {
+ const existing: AccountStorageV3 = {
version: 3,
activeIndex: 0,
- accounts: [{ accountId: "existing", refreshToken: "ref1", addedAt: 1, lastUsed: 2 }]
+ accounts: [{ accountId: "existing", refreshToken: "ref1", addedAt: 1, lastUsed: 2 }],
};
- // @ts-ignore
await saveAccounts(existing);
-
- const toImport = {
+
+ const toImport: AccountStorageV3 = {
version: 3,
activeIndex: 0,
- accounts: [{ accountId: "new", refreshToken: "ref2", addedAt: 3, lastUsed: 4 }]
+ accounts: [{ accountId: "new", refreshToken: "ref2", addedAt: 3, lastUsed: 4 }],
};
await fs.writeFile(exportPath, JSON.stringify(toImport));
-
- // @ts-ignore
+
await importAccounts(exportPath);
-
+
const loaded = await loadAccounts();
expect(loaded?.accounts).toHaveLength(2);
expect(loaded?.accounts.map(a => a.accountId)).toContain("new");
@@ -486,24 +469,20 @@ describe("storage", () => {
});
it("should enforce MAX_ACCOUNTS during import", async () => {
- // @ts-ignore
- const { importAccounts } = await import("../lib/storage.js");
-
const manyAccounts = Array.from({ length: 21 }, (_, i) => ({
accountId: `acct${i}`,
refreshToken: `ref${i}`,
addedAt: Date.now(),
- lastUsed: Date.now()
+ lastUsed: Date.now(),
}));
-
- const toImport = {
+
+ const toImport: AccountStorageV3 = {
version: 3,
activeIndex: 0,
- accounts: manyAccounts
+ accounts: manyAccounts,
};
await fs.writeFile(exportPath, JSON.stringify(toImport));
-
- // @ts-ignore
+
await expect(importAccounts(exportPath)).rejects.toThrow(/exceed maximum/);
});