From 90abdb8f1590db032a4141e3da531a28b63a71bf Mon Sep 17 00:00:00 2001 From: HwangJohn Date: Tue, 14 Jul 2026 21:16:24 +0900 Subject: [PATCH] fix: require complete chatgpt oauth startup tokens --- src/startup.ts | 37 ++++++++++++++++-- test/startup.test.ts | 93 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/startup.ts b/src/startup.ts index 4302ba74..65d17a51 100644 --- a/src/startup.ts +++ b/src/startup.ts @@ -1,8 +1,14 @@ import { shouldCheckUpdateNoop, getUpdateNoopStatus } from "./agent/utils.js"; +import { readCodexTokensFromEnv } from "./agent/openai-chatgpt-oauth.js"; import type { CliCommand } from "./commands.js"; import { + OPENAI_CHATGPT_ACCOUNT_ID_ENV_KEY, + OPENAI_CHATGPT_EXPIRES_AT_ENV_KEY, + OPENAI_CHATGPT_REFRESH_TOKEN_ENV_KEY, getProviderApiKeyEnvKey, + providerUsesOAuth, resolveConfiguredProvider, + type OpenWikiProvider, } from "./constants.js"; type ResolveStartupCommandOptions = { @@ -38,9 +44,12 @@ export async function resolveStartupCommand( ) { const provider = resolveConfiguredProvider(); const apiKeyEnvKey = getProviderApiKeyEnvKey(provider); - const hasProviderKey = Boolean(process.env[apiKeyEnvKey]); + const hasProviderCredentials = hasNonInteractiveProviderCredentials( + provider, + process.env, + ); - if (!hasProviderKey) { + if (!hasProviderCredentials) { if ( command.print && (await canSkipCleanUpdateBeforeCredentials( @@ -54,7 +63,7 @@ export async function resolveStartupCommand( return { kind: "error", exitCode: 1, - message: `${apiKeyEnvKey} is required for non-interactive runs. Run openwiki in an interactive terminal to save credentials.`, + message: `${formatCredentialRequirement(provider, apiKeyEnvKey)} is required for non-interactive runs. Run openwiki in an interactive terminal to save credentials.`, }; } } @@ -75,6 +84,28 @@ export async function resolveStartupCommand( return command; } +function hasNonInteractiveProviderCredentials( + provider: OpenWikiProvider, + env: NodeJS.ProcessEnv, +): boolean { + if (!providerUsesOAuth(provider)) { + return Boolean(env[getProviderApiKeyEnvKey(provider)]); + } + + return readCodexTokensFromEnv(env) !== null; +} + +function formatCredentialRequirement( + provider: OpenWikiProvider, + apiKeyEnvKey: string, +): string { + if (!providerUsesOAuth(provider)) { + return apiKeyEnvKey; + } + + return `A complete ChatGPT OAuth token set (${apiKeyEnvKey}, ${OPENAI_CHATGPT_REFRESH_TOKEN_ENV_KEY}, ${OPENAI_CHATGPT_EXPIRES_AT_ENV_KEY}, ${OPENAI_CHATGPT_ACCOUNT_ID_ENV_KEY})`; +} + async function canSkipCleanUpdateBeforeCredentials( command: Extract, cwd: string, diff --git a/test/startup.test.ts b/test/startup.test.ts index 2725a0b2..7f2650a5 100644 --- a/test/startup.test.ts +++ b/test/startup.test.ts @@ -6,10 +6,27 @@ import { promisify } from "node:util"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { resolveStartupCommand } from "../src/startup.ts"; import type { CliCommand } from "../src/commands.ts"; +import { + OPENAI_CHATGPT_ACCESS_TOKEN_ENV_KEY, + OPENAI_CHATGPT_ACCOUNT_ID_ENV_KEY, + OPENAI_CHATGPT_EXPIRES_AT_ENV_KEY, + OPENAI_CHATGPT_REFRESH_TOKEN_ENV_KEY, + OPENROUTER_API_KEY_ENV_KEY, + OPENWIKI_PROVIDER_ENV_KEY, +} from "../src/constants.ts"; const execFileAsync = promisify(execFile); -const originalProvider = process.env.OPENWIKI_PROVIDER; -const originalOpenRouterKey = process.env.OPENROUTER_API_KEY; +const MANAGED_ENV_KEYS = [ + OPENWIKI_PROVIDER_ENV_KEY, + OPENROUTER_API_KEY_ENV_KEY, + OPENAI_CHATGPT_ACCESS_TOKEN_ENV_KEY, + OPENAI_CHATGPT_REFRESH_TOKEN_ENV_KEY, + OPENAI_CHATGPT_EXPIRES_AT_ENV_KEY, + OPENAI_CHATGPT_ACCOUNT_ID_ENV_KEY, +] as const; +const originalEnv = new Map( + MANAGED_ENV_KEYS.map((key) => [key, process.env[key]]), +); async function git(cwd: string, args: string[]): Promise { const { stdout } = await execFileAsync("git", args, { cwd }); @@ -63,18 +80,34 @@ function updatePrintCommand( }; } +function setEnv(key: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } +} + +function storeChatGptTokens(expiresAtMs = Date.now() + 60 * 60 * 1000): void { + setEnv(OPENAI_CHATGPT_ACCESS_TOKEN_ENV_KEY, "access-token"); + setEnv(OPENAI_CHATGPT_REFRESH_TOKEN_ENV_KEY, "refresh-token"); + setEnv(OPENAI_CHATGPT_EXPIRES_AT_ENV_KEY, String(expiresAtMs)); + setEnv(OPENAI_CHATGPT_ACCOUNT_ID_ENV_KEY, "acct_1"); +} + beforeEach(() => { - process.env.OPENWIKI_PROVIDER = "openrouter"; - delete process.env.OPENROUTER_API_KEY; + process.env[OPENWIKI_PROVIDER_ENV_KEY] = "openrouter"; + delete process.env[OPENROUTER_API_KEY_ENV_KEY]; + delete process.env[OPENAI_CHATGPT_ACCESS_TOKEN_ENV_KEY]; + delete process.env[OPENAI_CHATGPT_REFRESH_TOKEN_ENV_KEY]; + delete process.env[OPENAI_CHATGPT_EXPIRES_AT_ENV_KEY]; + delete process.env[OPENAI_CHATGPT_ACCOUNT_ID_ENV_KEY]; }); afterEach(() => { - if (originalProvider === undefined) delete process.env.OPENWIKI_PROVIDER; - else process.env.OPENWIKI_PROVIDER = originalProvider; - - if (originalOpenRouterKey === undefined) - delete process.env.OPENROUTER_API_KEY; - else process.env.OPENROUTER_API_KEY = originalOpenRouterKey; + for (const key of MANAGED_ENV_KEYS) { + setEnv(key, originalEnv.get(key)); + } }); describe("resolveStartupCommand", () => { @@ -152,4 +185,44 @@ describe("resolveStartupCommand", () => { expect(result.message).toContain("OPENROUTER_API_KEY is required"); } }); + + test("rejects non-interactive ChatGPT OAuth startup with incomplete tokens", async () => { + process.env[OPENWIKI_PROVIDER_ENV_KEY] = "openai-chatgpt"; + process.env[OPENAI_CHATGPT_ACCESS_TOKEN_ENV_KEY] = "access-token"; + process.env[OPENAI_CHATGPT_EXPIRES_AT_ENV_KEY] = String( + Date.now() + 60 * 60 * 1000, + ); + + const result = await resolveStartupCommand( + updatePrintCommand({ userMessage: "refresh API docs" }), + { isStdinTTY: false }, + ); + + expect(result.kind).toBe("error"); + if (result.kind === "error") { + expect(result.message).toContain("ChatGPT OAuth token set"); + expect(result.message).toContain(OPENAI_CHATGPT_REFRESH_TOKEN_ENV_KEY); + expect(result.message).toContain(OPENAI_CHATGPT_ACCOUNT_ID_ENV_KEY); + } + }); + + test("allows non-interactive ChatGPT OAuth startup with complete tokens", async () => { + process.env[OPENWIKI_PROVIDER_ENV_KEY] = "openai-chatgpt"; + storeChatGptTokens(); + + const command = updatePrintCommand({ userMessage: "refresh API docs" }); + const result = await resolveStartupCommand(command, { isStdinTTY: false }); + + expect(result).toBe(command); + }); + + test("leaves expired complete ChatGPT OAuth tokens to the agent refresh path", async () => { + process.env[OPENWIKI_PROVIDER_ENV_KEY] = "openai-chatgpt"; + storeChatGptTokens(Date.now() - 60 * 1000); + + const command = updatePrintCommand({ userMessage: "refresh API docs" }); + const result = await resolveStartupCommand(command, { isStdinTTY: false }); + + expect(result).toBe(command); + }); });