From 658f34df404f17d7df5b49a68fc96f85673f5829 Mon Sep 17 00:00:00 2001 From: usr-bin-roygbiv Date: Sat, 25 Jul 2026 20:16:59 +0000 Subject: [PATCH 1/4] fix(ai): avoid OAuth barrel import cycle --- .../src/registry/oauth/anthropic-constants.ts | 12 ++++++++++ packages/ai/src/registry/oauth/anthropic.ts | 13 +---------- packages/ai/src/registry/oauth/index.ts | 2 +- packages/ai/test/oauth-barrel-import.test.ts | 22 +++++++++++++++++++ 4 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 packages/ai/src/registry/oauth/anthropic-constants.ts create mode 100644 packages/ai/test/oauth-barrel-import.test.ts diff --git a/packages/ai/src/registry/oauth/anthropic-constants.ts b/packages/ai/src/registry/oauth/anthropic-constants.ts new file mode 100644 index 000000000..e1a8f5d70 --- /dev/null +++ b/packages/ai/src/registry/oauth/anthropic-constants.ts @@ -0,0 +1,12 @@ +/** + * Absolute lifetime of an Anthropic OAuth grant family, anchored at the + * interactive login. Refresh-token rotation does NOT extend it: ~30 days + * after authorization the token endpoint returns + * `invalid_grant: "Refresh token expired"` for the latest rotated token and + * only a fresh interactive login recovers the account. Observed against + * production (grants authorized 2026-06-20/06-25 died 30d later to the hour + * despite healthy 8h rotations); matches Claude Code's documented monthly + * re-login. Consumers use this to warn before the deadline — it is a display + * heuristic, not a wire contract. + */ +export const ANTHROPIC_OAUTH_GRANT_TTL_MS = 30 * 24 * 60 * 60 * 1000; diff --git a/packages/ai/src/registry/oauth/anthropic.ts b/packages/ai/src/registry/oauth/anthropic.ts index 5c8658caf..a71ce833b 100644 --- a/packages/ai/src/registry/oauth/anthropic.ts +++ b/packages/ai/src/registry/oauth/anthropic.ts @@ -24,18 +24,7 @@ const CALLBACK_PATH = "/callback"; const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"; -/** - * Absolute lifetime of an Anthropic OAuth grant family, anchored at the - * interactive login. Refresh-token rotation does NOT extend it: ~30 days - * after authorization the token endpoint returns - * `invalid_grant: "Refresh token expired"` for the latest rotated token and - * only a fresh interactive login recovers the account. Observed against - * production (grants authorized 2026-06-20/06-25 died 30d later to the hour - * despite healthy 8h rotations); matches Claude Code's documented monthly - * re-login. Consumers use this to warn before the deadline — it is a display - * heuristic, not a wire contract. - */ -export const ANTHROPIC_OAUTH_GRANT_TTL_MS = 30 * 24 * 60 * 60 * 1000; +export { ANTHROPIC_OAUTH_GRANT_TTL_MS } from "./anthropic-constants"; function formatErrorDetails(error: unknown): string { if (error instanceof Error) { diff --git a/packages/ai/src/registry/oauth/index.ts b/packages/ai/src/registry/oauth/index.ts index 4b47d01de..534fffac4 100644 --- a/packages/ai/src/registry/oauth/index.ts +++ b/packages/ai/src/registry/oauth/index.ts @@ -12,7 +12,7 @@ import type { OAuthProviderInterface, } from "./types"; -export * from "./anthropic"; +export { ANTHROPIC_OAUTH_GRANT_TTL_MS } from "./anthropic-constants"; export * from "./device-code"; export type * from "./types"; diff --git a/packages/ai/test/oauth-barrel-import.test.ts b/packages/ai/test/oauth-barrel-import.test.ts new file mode 100644 index 000000000..5f2d7e8c2 --- /dev/null +++ b/packages/ai/test/oauth-barrel-import.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "bun:test"; + +const CONCURRENT_IMPORT_SCRIPT = ` +await Promise.all([ + import("@oh-my-pi/pi-ai/registry/oauth"), + import("@oh-my-pi/pi-ai/providers/anthropic"), + import("@oh-my-pi/pi-ai/auth-storage"), +]); +`; + +describe("OAuth barrel imports", () => { + it("loads concurrently with the Anthropic provider and auth storage", async () => { + const child = Bun.spawn([process.execPath, "-e", CONCURRENT_IMPORT_SCRIPT], { + cwd: import.meta.dir, + stdout: "pipe", + stderr: "pipe", + }); + const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]); + + expect(exitCode, stderr).toBe(0); + }); +}); From 8b77b3d80b353e5d393e90cef8696e98053fc991 Mon Sep 17 00:00:00 2001 From: usr-bin-roygbiv Date: Sat, 25 Jul 2026 20:29:26 +0000 Subject: [PATCH 2/4] fix(ai): preserve Anthropic OAuth exports --- packages/ai/src/providers/anthropic-constants.ts | 2 ++ packages/ai/src/providers/anthropic.ts | 3 ++- packages/ai/src/registry/oauth/anthropic.ts | 2 +- packages/ai/src/registry/oauth/index.ts | 2 +- packages/ai/test/fixtures/oauth-barrel-import.ts | 7 +++++++ packages/ai/test/oauth-barrel-import.test.ts | 13 ++++--------- 6 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 packages/ai/src/providers/anthropic-constants.ts create mode 100644 packages/ai/test/fixtures/oauth-barrel-import.ts diff --git a/packages/ai/src/providers/anthropic-constants.ts b/packages/ai/src/providers/anthropic-constants.ts new file mode 100644 index 000000000..a722c35a8 --- /dev/null +++ b/packages/ai/src/providers/anthropic-constants.ts @@ -0,0 +1,2 @@ +/** Claude Code version used by Anthropic wire fingerprints and OAuth bootstrap requests. */ +export const claudeCodeVersion = "2.1.165"; diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index b4980734e..f72a449c7 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -72,6 +72,7 @@ import { calculateAnthropicRetryDelayMs, retryDelayFromHeaders, } from "./anthropic-client"; +import { claudeCodeVersion } from "./anthropic-constants"; import { type ToolInputSchema as AnthropicToolInputSchema, type Tool as AnthropicWireTool, @@ -464,7 +465,7 @@ function getCacheControl( } // Stealth mode: mimic Claude Code's request fingerprint. -export const claudeCodeVersion = "2.1.165"; +export { claudeCodeVersion }; export const claudeAgentSdkVersion = "0.3.165"; export const claudeClientVersion = "1.11187.4"; export const claudeToolPrefix: string = "_"; diff --git a/packages/ai/src/registry/oauth/anthropic.ts b/packages/ai/src/registry/oauth/anthropic.ts index a71ce833b..e31df5e47 100644 --- a/packages/ai/src/registry/oauth/anthropic.ts +++ b/packages/ai/src/registry/oauth/anthropic.ts @@ -3,7 +3,7 @@ */ import * as AIError from "../../error"; -import { claudeCodeVersion } from "../../providers/anthropic"; +import { claudeCodeVersion } from "../../providers/anthropic-constants"; import type { FetchImpl } from "../../types"; import { OAuthCallbackFlow } from "./callback-server"; import { generatePKCE } from "./pkce"; diff --git a/packages/ai/src/registry/oauth/index.ts b/packages/ai/src/registry/oauth/index.ts index 534fffac4..4b47d01de 100644 --- a/packages/ai/src/registry/oauth/index.ts +++ b/packages/ai/src/registry/oauth/index.ts @@ -12,7 +12,7 @@ import type { OAuthProviderInterface, } from "./types"; -export { ANTHROPIC_OAUTH_GRANT_TTL_MS } from "./anthropic-constants"; +export * from "./anthropic"; export * from "./device-code"; export type * from "./types"; diff --git a/packages/ai/test/fixtures/oauth-barrel-import.ts b/packages/ai/test/fixtures/oauth-barrel-import.ts new file mode 100644 index 000000000..758a904c0 --- /dev/null +++ b/packages/ai/test/fixtures/oauth-barrel-import.ts @@ -0,0 +1,7 @@ +import { AnthropicOAuthFlow, loginAnthropic, refreshAnthropicToken } from "@oh-my-pi/pi-ai/registry/oauth"; +import "@oh-my-pi/pi-ai/providers/anthropic"; +import "@oh-my-pi/pi-ai/auth-storage"; + +if (!AnthropicOAuthFlow || !loginAnthropic || !refreshAnthropicToken) { + throw new Error("Anthropic OAuth exports are unavailable"); +} diff --git a/packages/ai/test/oauth-barrel-import.test.ts b/packages/ai/test/oauth-barrel-import.test.ts index 5f2d7e8c2..57d66c971 100644 --- a/packages/ai/test/oauth-barrel-import.test.ts +++ b/packages/ai/test/oauth-barrel-import.test.ts @@ -1,16 +1,11 @@ import { describe, expect, it } from "bun:test"; +import { fileURLToPath } from "node:url"; -const CONCURRENT_IMPORT_SCRIPT = ` -await Promise.all([ - import("@oh-my-pi/pi-ai/registry/oauth"), - import("@oh-my-pi/pi-ai/providers/anthropic"), - import("@oh-my-pi/pi-ai/auth-storage"), -]); -`; +const STATIC_IMPORT_FIXTURE = fileURLToPath(new URL("./fixtures/oauth-barrel-import.ts", import.meta.url)); describe("OAuth barrel imports", () => { - it("loads concurrently with the Anthropic provider and auth storage", async () => { - const child = Bun.spawn([process.execPath, "-e", CONCURRENT_IMPORT_SCRIPT], { + it("loads with the Anthropic provider and auth storage while preserving public exports", async () => { + const child = Bun.spawn([process.execPath, STATIC_IMPORT_FIXTURE], { cwd: import.meta.dir, stdout: "pipe", stderr: "pipe", From 5f247ac77f31850a5c222cf19435c8a006b4c85e Mon Sep 17 00:00:00 2001 From: usr-bin-roygbiv Date: Sat, 25 Jul 2026 20:47:38 +0000 Subject: [PATCH 3/4] test(ai): use Bun-native fixture path --- packages/ai/test/oauth-barrel-import.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ai/test/oauth-barrel-import.test.ts b/packages/ai/test/oauth-barrel-import.test.ts index 57d66c971..dcf511943 100644 --- a/packages/ai/test/oauth-barrel-import.test.ts +++ b/packages/ai/test/oauth-barrel-import.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "bun:test"; -import { fileURLToPath } from "node:url"; -const STATIC_IMPORT_FIXTURE = fileURLToPath(new URL("./fixtures/oauth-barrel-import.ts", import.meta.url)); +const STATIC_IMPORT_FIXTURE = `${import.meta.dir}/fixtures/oauth-barrel-import.ts`; describe("OAuth barrel imports", () => { it("loads with the Anthropic provider and auth storage while preserving public exports", async () => { From 19beceb00afc58970cf9ff38b5956e4b9574933f Mon Sep 17 00:00:00 2001 From: usr-bin-roygbiv Date: Sun, 26 Jul 2026 21:05:10 +0000 Subject: [PATCH 4/4] test(ai): cover both public OAuth barrels --- .../ai/test/fixtures/oauth-barrel-import.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/ai/test/fixtures/oauth-barrel-import.ts b/packages/ai/test/fixtures/oauth-barrel-import.ts index 758a904c0..074377e70 100644 --- a/packages/ai/test/fixtures/oauth-barrel-import.ts +++ b/packages/ai/test/fixtures/oauth-barrel-import.ts @@ -1,7 +1,25 @@ -import { AnthropicOAuthFlow, loginAnthropic, refreshAnthropicToken } from "@oh-my-pi/pi-ai/registry/oauth"; +import { + AnthropicOAuthFlow as RootAnthropicOAuthFlow, + loginAnthropic as rootLoginAnthropic, + refreshAnthropicToken as rootRefreshAnthropicToken, +} from "@oh-my-pi/pi-ai"; +import { + AnthropicOAuthFlow as OAuthAnthropicOAuthFlow, + loginAnthropic as oauthLoginAnthropic, + refreshAnthropicToken as oauthRefreshAnthropicToken, +} from "@oh-my-pi/pi-ai/registry/oauth"; import "@oh-my-pi/pi-ai/providers/anthropic"; import "@oh-my-pi/pi-ai/auth-storage"; -if (!AnthropicOAuthFlow || !loginAnthropic || !refreshAnthropicToken) { +const publicExports = [ + RootAnthropicOAuthFlow, + rootLoginAnthropic, + rootRefreshAnthropicToken, + OAuthAnthropicOAuthFlow, + oauthLoginAnthropic, + oauthRefreshAnthropicToken, +]; + +if (publicExports.some(value => !value)) { throw new Error("Anthropic OAuth exports are unavailable"); }