Skip to content

Commit 6dd26d2

Browse files
committed
chore(auth): pin oauth-core to npm 0.2.0
1 parent 83c2aed commit 6dd26d2

13 files changed

Lines changed: 214 additions & 58 deletions

‎bun.lock‎

Lines changed: 90 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"dependencies": {
8989
"@corbits/agent-intern": "workspace:*",
9090
"@corbits/codex-provider": "github:corbitsdev/corbits-codex-provider",
91-
"@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core",
91+
"@corbits/oauth-core": "0.2.0",
9292
"@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses",
9393
"@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider",
9494
"@intx/agent": "workspace:*",

‎src/auth/codex/callback-server.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
callbackPageHtml,
66
type CallbackPageCopy,
77
} from "../callback-page.js";
8-
import { CODEX_CALLBACK_PATH, CODEX_CALLBACK_PORT } from "./constants.js";
8+
import {
9+
CODEX_CALLBACK_HOST,
10+
CODEX_CALLBACK_PATH,
11+
CODEX_CALLBACK_PORT,
12+
} from "./constants.js";
913

1014
export type CodexCallbackServer = CallbackServer;
1115

@@ -18,7 +22,7 @@ export async function startCodexCallbackServer(
1822
return startCallbackServer(expectedState, {
1923
port: CODEX_CALLBACK_PORT,
2024
// Codex's registered redirect_uri uses localhost (not 127.0.0.1).
21-
host: "localhost",
25+
host: CODEX_CALLBACK_HOST,
2226
path: CODEX_CALLBACK_PATH,
2327
doneHtml: authorizationDoneHtml("Codex", copy),
2428
failedHtml: (reason) =>

‎src/auth/codex/constants.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { CODEX_REDIRECT_URI } from "@corbits/codex-provider";
1+
import { callbackTargetFor } from "@corbits/oauth-core";
2+
import { codexOAuthConfig } from "@corbits/codex-provider";
23

34
export {
45
CODEX_BASE_URL,
@@ -7,9 +8,10 @@ export {
78
CODEX_RESPONSES_PATH,
89
} from "@corbits/codex-provider";
910

10-
const codexRedirect = new URL(CODEX_REDIRECT_URI);
11-
export const CODEX_CALLBACK_PORT = Number(codexRedirect.port);
12-
export const CODEX_CALLBACK_PATH = codexRedirect.pathname;
11+
const callbackTarget = callbackTargetFor(codexOAuthConfig);
12+
export const CODEX_CALLBACK_HOST = callbackTarget.host;
13+
export const CODEX_CALLBACK_PORT = callbackTarget.port;
14+
export const CODEX_CALLBACK_PATH = callbackTarget.path;
1315

1416
// The account's available model catalog. The models endpoint requires a
1517
// client_version query param.

‎src/auth/codex/session-failure.test.ts‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ import { mkdtemp, rm } from "node:fs/promises";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import { describe, expect, test } from "bun:test";
5-
import {
6-
OAuthRefreshFailedError,
7-
OAuthTokenEndpointError,
8-
} from "@corbits/oauth-core";
5+
import { OAuthRefreshFailedError } from "@corbits/oauth-core";
96
import { errorMessage } from "../../agent/error-message.js";
107
import { saveCodexProfile } from "../../config/oauth-stores.js";
118
import { formatSubAgentSpawnAuthFailureMessage } from "../../subagent/inference-auth-failure.js";
9+
import { isOAuthTokenEndpointError } from "../token-session-boundary.js";
1210
import {
1311
codexAuthFailureDiagnostic,
1412
CodexAuthError,
@@ -150,21 +148,21 @@ describe("codex auth failure surface", () => {
150148
.catch((error: unknown) => error);
151149
expect(normal).toBeInstanceOf(OAuthRefreshFailedError);
152150
const normalCause = (normal as OAuthRefreshFailedError).cause;
153-
expect(normalCause).toBeInstanceOf(OAuthTokenEndpointError);
151+
expect(isOAuthTokenEndpointError(normalCause)).toBe(true);
154152
expect(normalCause).toMatchObject({ status: 401 });
155153

156154
const staged = await refreshStagedCodexTokens({ ...tokens }, now).catch(
157155
(error: unknown) => error,
158156
);
159-
expect(staged).toBeInstanceOf(OAuthTokenEndpointError);
157+
expect(isOAuthTokenEndpointError(staged)).toBe(true);
160158
expect(staged).toMatchObject({ status: 401 });
161159

162160
for (const failure of [normal, staged]) {
163161
let current: unknown = failure;
164162
while (current instanceof Error) {
165163
expect(current.message).not.toContain(refresh);
166164
expect(current.stack).not.toContain(refresh);
167-
if (current instanceof OAuthTokenEndpointError)
165+
if (isOAuthTokenEndpointError(current))
168166
expect(current.detail).not.toContain(refresh);
169167
current = current.cause;
170168
}

‎src/auth/oauth-scope-check.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// Never logs or persists the token or any response body — only the HTTP
1111
// status is inspected to classify the result.
1212

13-
import { OAuthTokenEndpointError } from "@corbits/oauth-core";
13+
import { isOAuthTokenEndpointError } from "./token-session-boundary.js";
1414

1515
import {
1616
CODEX_BASE_URL,
@@ -81,7 +81,7 @@ export function isBlockingOAuthScopeCheckResult(
8181
}
8282

8383
function isDefinitiveRefreshAuthRejection(err: unknown): boolean {
84-
if (!(err instanceof OAuthTokenEndpointError)) return false;
84+
if (!isOAuthTokenEndpointError(err)) return false;
8585
if (err.status === 401 || err.status === 403) return true;
8686
return /invalid_grant|revoked/i.test(err.detail);
8787
}

‎src/auth/token-session-boundary.test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { describe, expect, test } from "bun:test";
22
import { OAuthTokenEndpointError } from "@corbits/oauth-core";
3-
import { sanitizedRefreshFailure } from "./token-session-boundary.js";
3+
import {
4+
isOAuthTokenEndpointError,
5+
sanitizedRefreshFailure,
6+
} from "./token-session-boundary.js";
47

58
describe("sanitizedRefreshFailure", () => {
69
test("scrubs already-materialized stacks recursively without losing classification", () => {
@@ -28,3 +31,27 @@ describe("sanitizedRefreshFailure", () => {
2831
expect(endpoint.stack).not.toContain(refresh);
2932
});
3033
});
34+
35+
describe("isOAuthTokenEndpointError", () => {
36+
test("accepts the host class and a foreign copy with the same name", () => {
37+
const local = new OAuthTokenEndpointError(401, "denied");
38+
expect(isOAuthTokenEndpointError(local)).toBe(true);
39+
40+
const foreign = Object.assign(
41+
new Error("OAuth token endpoint returned 401"),
42+
{
43+
name: "OAuthTokenEndpointError",
44+
status: 401,
45+
detail: "denied",
46+
},
47+
);
48+
expect(foreign).not.toBeInstanceOf(OAuthTokenEndpointError);
49+
expect(isOAuthTokenEndpointError(foreign)).toBe(true);
50+
});
51+
52+
test("rejects errors that only share a message", () => {
53+
expect(
54+
isOAuthTokenEndpointError(new Error("OAuth token endpoint returned 401")),
55+
).toBe(false);
56+
});
57+
});

‎src/auth/token-session-boundary.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ export function replaceMutableTokens<TTokens extends object>(
1010
Object.assign(target, replacement);
1111
}
1212

13+
// Sibling provider packages may still ship a distinct @corbits/oauth-core
14+
// copy, so `instanceof` against this host's class identity is not reliable.
15+
export type OAuthTokenEndpointFailure = Error & {
16+
status: number;
17+
detail: string;
18+
};
19+
20+
export function isOAuthTokenEndpointError(
21+
err: unknown,
22+
): err is OAuthTokenEndpointFailure {
23+
if (!(err instanceof Error) || err.name !== "OAuthTokenEndpointError")
24+
return false;
25+
if (!("status" in err) || typeof err.status !== "number") return false;
26+
return "detail" in err && typeof err.detail === "string";
27+
}
28+
1329
export function sanitizedRefreshFailure(
1430
error: unknown,
1531
refreshToken: string,

‎src/auth/xai/callback-server.test.ts‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { describe, expect, test } from "bun:test";
2+
import { setTimeout as delay } from "node:timers/promises";
23

34
import type { CallbackPageCopy } from "../callback-page.js";
4-
import { XAI_CALLBACK_PORT } from "./constants.js";
5+
import { XAI_CALLBACK_PATH, XAI_CALLBACK_PORT } from "./constants.js";
56
import { startXaiCallbackServer } from "./callback-server.js";
67

78
const copy: CallbackPageCopy = {
@@ -12,12 +13,13 @@ const copy: CallbackPageCopy = {
1213
githubLabel: "github.com/fixture",
1314
};
1415

15-
const base = `http://127.0.0.1:${String(XAI_CALLBACK_PORT)}/callback`;
16+
const base = `http://127.0.0.1:${String(XAI_CALLBACK_PORT)}${XAI_CALLBACK_PATH}`;
1617

1718
describe("xAI callback server", () => {
1819
test("accepts a matching state and returns the code", async () => {
1920
const server = await startXaiCallbackServer("expected", copy);
2021
try {
22+
expect(server.port).toBe(XAI_CALLBACK_PORT);
2123
const wait = server.waitForCode(new AbortController().signal);
2224
const res = await fetch(`${base}?code=abc&state=expected`);
2325
expect(res.status).toBe(200);
@@ -27,20 +29,21 @@ describe("xAI callback server", () => {
2729
}
2830
});
2931

30-
test("rejects state mismatches before accepting a code", async () => {
32+
test("keeps waiting after a state mismatch until a matching redirect", async () => {
3133
const server = await startXaiCallbackServer("expected", copy);
3234
try {
33-
const wait = server.waitForCode(new AbortController().signal).then(
34-
() => ({ ok: true as const }),
35-
(err: unknown) => ({ ok: false as const, err }),
36-
);
37-
const res = await fetch(`${base}?code=abc&state=wrong`);
38-
expect(res.status).toBe(400);
39-
const result = await wait;
40-
expect(result.ok).toBe(false);
41-
if (!result.ok) expect(result.err).toBeInstanceOf(Error);
42-
if (!result.ok && result.err instanceof Error)
43-
expect(result.err.message).toMatch(/state did not match/);
35+
const wait = server.waitForCode(new AbortController().signal);
36+
const mismatch = await fetch(`${base}?code=abc&state=wrong`);
37+
expect(mismatch.status).toBe(400);
38+
expect(
39+
await Promise.race([
40+
wait.then(() => "settled"),
41+
delay(50).then(() => "pending"),
42+
]),
43+
).toBe("pending");
44+
const match = await fetch(`${base}?code=abc&state=expected`);
45+
expect(match.status).toBe(200);
46+
await expect(wait).resolves.toBe("abc");
4447
} finally {
4548
server.close();
4649
}

‎src/auth/xai/callback-server.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
callbackPageHtml,
66
type CallbackPageCopy,
77
} from "../callback-page.js";
8-
import { XAI_CALLBACK_PATH, XAI_CALLBACK_PORT } from "./constants.js";
8+
import {
9+
XAI_CALLBACK_HOST,
10+
XAI_CALLBACK_PATH,
11+
XAI_CALLBACK_PORT,
12+
} from "./constants.js";
913

1014
export type XaiCallbackServer = CallbackServer;
1115

@@ -15,7 +19,7 @@ export async function startXaiCallbackServer(
1519
): Promise<XaiCallbackServer> {
1620
return startCallbackServer(expectedState, {
1721
port: XAI_CALLBACK_PORT,
18-
host: "127.0.0.1",
22+
host: XAI_CALLBACK_HOST,
1923
path: XAI_CALLBACK_PATH,
2024
doneHtml: authorizationDoneHtml("xAI", copy),
2125
failedHtml: (reason) =>

0 commit comments

Comments
 (0)