Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/reference/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Stable APIs are covered by semver compatibility guarantees and must remain backw
- `OpenAIOAuthPlugin`
- `OpenAIAuthPlugin`
- default export (alias of `OpenAIOAuthPlugin`)
- Supported package subpath entrypoints:
- `codex-multi-auth/auth`
- `codex-multi-auth/storage`
- `codex-multi-auth/config`
- `codex-multi-auth/request`
- `codex-multi-auth/cli`
- CLI surface:
- `codex auth ...` command family
- documented flags and aliases in `reference/commands.md`
Expand Down
1 change: 1 addition & 0 deletions lib/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./auth.js";
4 changes: 4 additions & 0 deletions lib/codex-cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./observability.js";
export * from "./state.js";
export * from "./sync.js";
export * from "./writer.js";
4 changes: 4 additions & 0 deletions lib/request/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./failure-policy.js";
export * from "./fetch-helpers.js";
export * from "./rate-limit-backoff.js";
export * from "./request-transformer.js";
33 changes: 33 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@
"description": "Multi-account OAuth manager and codex auth wrapper for the official @openai/codex CLI, with switching, health checks, and recovery tools",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./auth": {
"types": "./dist/lib/auth/index.d.ts",
"import": "./dist/lib/auth/index.js",
"default": "./dist/lib/auth/index.js"
},
"./storage": {
"types": "./dist/lib/storage.d.ts",
"import": "./dist/lib/storage.js",
"default": "./dist/lib/storage.js"
},
"./config": {
"types": "./dist/lib/config.d.ts",
"import": "./dist/lib/config.js",
"default": "./dist/lib/config.js"
},
"./request": {
"types": "./dist/lib/request/index.d.ts",
"import": "./dist/lib/request/index.js",
"default": "./dist/lib/request/index.js"
},
"./cli": {
"types": "./dist/lib/codex-cli/index.d.ts",
"import": "./dist/lib/codex-cli/index.js",
"default": "./dist/lib/codex-cli/index.js"
},
"./package.json": "./package.json"
},
"type": "module",
"license": "MIT",
"author": "ndycode",
Expand Down
5 changes: 5 additions & 0 deletions test/documentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ describe("Documentation Integrity", () => {
expect(publicApi).toContain("tier c");
expect(publicApi).toContain("options-object");
expect(publicApi).toContain("semver");
expect(publicApi).toContain("codex-multi-auth/auth");
expect(publicApi).toContain("codex-multi-auth/storage");
expect(publicApi).toContain("codex-multi-auth/config");
expect(publicApi).toContain("codex-multi-auth/request");
expect(publicApi).toContain("codex-multi-auth/cli");

expect(errorContracts).toContain("exit codes");
expect(errorContracts).toContain("json mode contract");
Expand Down
139 changes: 112 additions & 27 deletions test/public-api-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { describe, expect, it, vi } from "vitest";
import {
HealthScoreTracker,
TokenBucketTracker,
exponentialBackoff,
selectHybridAccount,
} from "../lib/rotation.js";
import { getTopCandidates } from "../lib/parallel-probe.js";
import { createCodexHeaders } from "../lib/request/fetch-helpers.js";
import {
clearRateLimitBackoffState,
getRateLimitBackoffWithReason,
} from "../lib/request/rate-limit-backoff.js";
import { transformRequestBody } from "../lib/request/request-transformer.js";
import {
exponentialBackoff,
HealthScoreTracker,
selectHybridAccount,
TokenBucketTracker,
} from "../lib/rotation.js";
import type { RequestBody } from "../lib/types.js";
import pkg from "../package.json" with { type: "json" };

describe("public api contract", () => {
it("keeps root plugin exports aligned", async () => {
Expand All @@ -26,37 +27,108 @@ describe("public api contract", () => {
const rotation = await import("../lib/rotation.js");
const parallelProbe = await import("../lib/parallel-probe.js");
const fetchHelpers = await import("../lib/request/fetch-helpers.js");
const rateLimitBackoff = await import("../lib/request/rate-limit-backoff.js");
const requestTransformer = await import("../lib/request/request-transformer.js");
const required: ReadonlyArray<
readonly [string, Record<string, unknown>]
> = [
["selectHybridAccount", rotation],
["exponentialBackoff", rotation],
["getTopCandidates", parallelProbe],
["createCodexHeaders", fetchHelpers],
["getRateLimitBackoffWithReason", rateLimitBackoff],
["transformRequestBody", requestTransformer],
];
const rateLimitBackoff = await import(
"../lib/request/rate-limit-backoff.js"
);
const requestTransformer = await import(
"../lib/request/request-transformer.js"
);
const required: ReadonlyArray<readonly [string, Record<string, unknown>]> =
[
["selectHybridAccount", rotation],
["exponentialBackoff", rotation],
["getTopCandidates", parallelProbe],
["createCodexHeaders", fetchHelpers],
["getRateLimitBackoffWithReason", rateLimitBackoff],
["transformRequestBody", requestTransformer],
];
for (const [name, mod] of required) {
expect(name in mod, `missing export: ${name}`).toBe(true);
expect(typeof mod[name], `${name} should be a function`).toBe("function");
}
});

it("declares the supported package subpath exports", async () => {
expect(pkg.exports).toEqual({
".": {
types: "./dist/index.d.ts",
import: "./dist/index.js",
default: "./dist/index.js",
},
"./auth": {
types: "./dist/lib/auth/index.d.ts",
import: "./dist/lib/auth/index.js",
default: "./dist/lib/auth/index.js",
},
"./storage": {
types: "./dist/lib/storage.d.ts",
import: "./dist/lib/storage.js",
default: "./dist/lib/storage.js",
},
"./config": {
types: "./dist/lib/config.d.ts",
import: "./dist/lib/config.js",
default: "./dist/lib/config.js",
},
"./request": {
types: "./dist/lib/request/index.d.ts",
import: "./dist/lib/request/index.js",
default: "./dist/lib/request/index.js",
},
"./cli": {
types: "./dist/lib/codex-cli/index.d.ts",
import: "./dist/lib/codex-cli/index.js",
default: "./dist/lib/codex-cli/index.js",
},
"./package.json": "./package.json",
});
});

it("keeps the supported subpath entry barrels aligned", async () => {
const auth = await import("../lib/auth/index.js");
const storage = await import("../lib/storage.js");
const config = await import("../lib/config.js");
const request = await import("../lib/request/index.js");
const cli = await import("../lib/codex-cli/index.js");

expect(typeof auth.exchangeAuthorizationCode).toBe("function");
expect(typeof storage.loadAccounts).toBe("function");
expect(typeof config.loadPluginConfig).toBe("function");
expect(typeof request.createCodexHeaders).toBe("function");
expect(typeof request.transformRequestBody).toBe("function");
expect("handleResponse" in request).toBe(false);
expect("withStreamFailover" in request).toBe(false);
expect(typeof cli.loadCodexCliState).toBe("function");
});

it("keeps positional and options-object overload behavior aligned", async () => {
const healthTracker = new HealthScoreTracker();
const tokenTracker = new TokenBucketTracker();
const accounts = [{ index: 0, isAvailable: true, lastUsed: 1_709_280_000_000 }];
const accounts = [
{ index: 0, isAvailable: true, lastUsed: 1_709_280_000_000 },
];

const selectedPositional = selectHybridAccount(accounts, healthTracker, tokenTracker);
const selectedNamed = selectHybridAccount({ accounts, healthTracker, tokenTracker });
const selectedPositional = selectHybridAccount(
accounts,
healthTracker,
tokenTracker,
);
const selectedNamed = selectHybridAccount({
accounts,
healthTracker,
tokenTracker,
});
expect(selectedNamed?.index).toBe(selectedPositional?.index);

const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0.5);
try {
const backoffPositional = exponentialBackoff(3, 1000, 60000, 0);
const backoffNamed = exponentialBackoff({ attempt: 3, baseMs: 1000, maxMs: 60000, jitterFactor: 0 });
const backoffNamed = exponentialBackoff({
attempt: 3,
baseMs: 1000,
maxMs: 60000,
jitterFactor: 0,
});
expect(backoffNamed).toBe(backoffPositional);
} finally {
randomSpy.mockRestore();
Expand All @@ -82,7 +154,9 @@ describe("public api contract", () => {
1,
);
const topNamed = getTopCandidates({
accountManager: manager as unknown as Parameters<typeof getTopCandidates>[0],
accountManager: manager as unknown as Parameters<
typeof getTopCandidates
>[0],
modelFamily: "codex",
model: null,
maxCandidates: 1,
Expand All @@ -99,11 +173,22 @@ describe("public api contract", () => {
accessToken: "token",
opts: { model: "gpt-5", promptCacheKey: "session-compat" },
});
expect(headersNamed.get("Authorization")).toBe(headersPositional.get("Authorization"));
expect(headersNamed.get("conversation_id")).toBe(headersPositional.get("conversation_id"));
expect(headersNamed.get("session_id")).toBe(headersPositional.get("session_id"));
expect(headersNamed.get("Authorization")).toBe(
headersPositional.get("Authorization"),
);
expect(headersNamed.get("conversation_id")).toBe(
headersPositional.get("conversation_id"),
);
expect(headersNamed.get("session_id")).toBe(
headersPositional.get("session_id"),
);

const ratePositional = getRateLimitBackoffWithReason(1, "compat", 1000, "tokens");
const ratePositional = getRateLimitBackoffWithReason(
1,
"compat",
1000,
"tokens",
);
clearRateLimitBackoffState();
const rateNamed = getRateLimitBackoffWithReason({
accountIndex: 1,
Expand Down