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/); });