mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 08:24:46 +00:00
fix(usage): keep account identity aligned with credentials
Read labels from local token claims and use one credential lookup for usage fetches and account identification. Bound cached profile identities across token rotation.
This commit is contained in:
@@ -647,3 +647,131 @@ it("token-only Claude inputs use a cached OAuth profile identity", async () => {
|
||||
});
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
|
||||
it("identifies the same configDir credentials that fetch uses when oauthAccount is absent", async () => {
|
||||
const { identify } = await import("./usage.js");
|
||||
const directory = mkdtempSync(join(tmpdir(), "claude-credential-identity-"));
|
||||
try {
|
||||
writeClaudeCredentials(directory, "fixture-credential-token");
|
||||
writeFileSync(join(directory, ".claude.json"), JSON.stringify({}));
|
||||
let usageRequested = false;
|
||||
await fetchUsage({ configDir: directory }, async () => {
|
||||
usageRequested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
});
|
||||
expect(usageRequested).toBe(true);
|
||||
let profileRequested = false;
|
||||
const account = await identify({ configDir: directory }, async () => {
|
||||
profileRequested = true;
|
||||
return jsonResponse({
|
||||
account: { uuid: "account-uuid", email: "owner@example.test" },
|
||||
organization: { uuid: "org-uuid" },
|
||||
});
|
||||
});
|
||||
expect(profileRequested).toBe(true);
|
||||
expect(account).toEqual({ key: "account-uuid.org-uuid", label: "owner@example.test" });
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("expires cached token profiles after five minutes", async () => {
|
||||
const { identify } = await import("./usage.js");
|
||||
let now = 0;
|
||||
let calls = 0;
|
||||
const fetchProfile: typeof fetch = async () => {
|
||||
calls++;
|
||||
return jsonResponse({ account: { uuid: "account-uuid" }, organization: { uuid: "org-uuid" } });
|
||||
};
|
||||
const input = { accessToken: "ttl-fixture-token" };
|
||||
await identify(input, fetchProfile, () => now);
|
||||
await identify(input, fetchProfile, () => now);
|
||||
expect(calls).toBe(1);
|
||||
now = 300_001;
|
||||
await identify(input, fetchProfile, () => now);
|
||||
expect(calls).toBe(2);
|
||||
});
|
||||
|
||||
it("identify and fetch share the macOS keychain credential fallback", async () => {
|
||||
const { identify } = await import("./usage.js");
|
||||
const directory = mkdtempSync(join(tmpdir(), "claude-keychain-identity-"));
|
||||
try {
|
||||
const lookup = {
|
||||
platform: "darwin" as const,
|
||||
claudeHome: directory,
|
||||
accountHome: directory,
|
||||
readKeychainCredentials: async () => ({
|
||||
claudeAiOauth: { accessToken: "keychain-fixture-token" },
|
||||
}),
|
||||
};
|
||||
let usageRequested = false;
|
||||
await fetchUsage(
|
||||
{},
|
||||
async () => {
|
||||
usageRequested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
},
|
||||
lookup,
|
||||
);
|
||||
expect(usageRequested).toBe(true);
|
||||
const account = await identify(
|
||||
{},
|
||||
async () =>
|
||||
jsonResponse({
|
||||
account: { uuid: "keychain-account" },
|
||||
organization: { uuid: "keychain-org" },
|
||||
}),
|
||||
Date.now,
|
||||
lookup,
|
||||
);
|
||||
expect(account).toEqual({ key: "keychain-account.keychain-org" });
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("identify and fetch share the default Claude home credentials", async () => {
|
||||
const { identify } = await import("./usage.js");
|
||||
const directory = mkdtempSync(join(tmpdir(), "claude-home-identity-"));
|
||||
try {
|
||||
writeClaudeCredentials(directory, "home-fixture-token");
|
||||
const lookup = { claudeHome: directory, accountHome: directory };
|
||||
let usageRequested = false;
|
||||
await fetchUsage(
|
||||
{},
|
||||
async () => {
|
||||
usageRequested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
},
|
||||
lookup,
|
||||
);
|
||||
expect(usageRequested).toBe(true);
|
||||
const identity = await identify(
|
||||
{},
|
||||
async () =>
|
||||
jsonResponse({
|
||||
account: { uuid: "home-account" },
|
||||
organization: { uuid: "home-org" },
|
||||
}),
|
||||
Date.now,
|
||||
lookup,
|
||||
);
|
||||
expect(identity).toEqual({ key: "home-account.home-org" });
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds cached profiles across many token rotations", async () => {
|
||||
const { identify } = await import("./usage.js");
|
||||
let calls = 0;
|
||||
const fetchProfile: typeof fetch = async () => {
|
||||
calls++;
|
||||
return jsonResponse({ account: { uuid: "account" }, organization: { uuid: "org" } });
|
||||
};
|
||||
for (let index = 0; index < 129; index++) {
|
||||
await identify({ accessToken: `rotation-fixture-${index}` }, fetchProfile);
|
||||
}
|
||||
await identify({ accessToken: "rotation-fixture-0" }, fetchProfile);
|
||||
expect(calls).toBe(130);
|
||||
});
|
||||
|
||||
@@ -327,16 +327,51 @@ export async function readClaudeKeychainCredentials(
|
||||
return null;
|
||||
}
|
||||
|
||||
interface ClaudeCredentialLookup {
|
||||
platform?: NodeJS.Platform;
|
||||
readKeychainCredentials?: () => Promise<unknown | null>;
|
||||
claudeHome?: string;
|
||||
accountHome?: string;
|
||||
}
|
||||
|
||||
/** Shared credential lookup for usage fetches and account identification. */
|
||||
export async function resolveClaudeCredentials(
|
||||
input: UsageInput,
|
||||
lookup: ClaudeCredentialLookup = {},
|
||||
): Promise<ClaudeCredentialRecord | null> {
|
||||
if ("accessToken" in input) return { oauth: { accessToken: input.accessToken } };
|
||||
if ("configDir" in input) return readCredentialFile(join(input.configDir, ".credentials.json"));
|
||||
const claudeHome = lookup.claudeHome ?? process.env["CLAUDE_HOME"] ?? join(homedir(), ".claude");
|
||||
const fileCredentials = await readCredentialFile(join(claudeHome, ".credentials.json"));
|
||||
if (fileCredentials) return fileCredentials;
|
||||
if ((lookup.platform ?? process.platform) !== "darwin") return null;
|
||||
const parsed = ClaudeCredentialsSchema.safeParse(
|
||||
await (lookup.readKeychainCredentials ?? readClaudeKeychainCredentials)(),
|
||||
);
|
||||
return parsed.success ? toCredentialRecord(parsed.data) : null;
|
||||
}
|
||||
|
||||
async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord | null> {
|
||||
if (!existsSync(path)) return null;
|
||||
try {
|
||||
return toCredentialRecord(
|
||||
ClaudeCredentialsSchema.parse(JSON.parse(await fs.readFile(path, "utf8"))),
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function toCredentialRecord(credentials: ClaudeCredentials): ClaudeCredentialRecord | null {
|
||||
const oauth = credentials.claudeAiOauth;
|
||||
return oauth?.accessToken ? { oauth: { ...oauth, accessToken: oauth.accessToken } } : null;
|
||||
}
|
||||
|
||||
export async function fetchUsage(
|
||||
input: UsageInput,
|
||||
fetchApi: typeof fetch = fetch,
|
||||
credentialLookup: ClaudeCredentialLookup = {},
|
||||
): Promise<UsageReport> {
|
||||
const claudeHome = process.env["CLAUDE_HOME"] || join(homedir(), ".claude");
|
||||
const configDir = "configDir" in input ? input.configDir : undefined;
|
||||
const accessToken = "accessToken" in input ? input.accessToken : undefined;
|
||||
const readKeychainCredentials = readClaudeKeychainCredentials;
|
||||
const platform = process.platform;
|
||||
|
||||
/**
|
||||
* Scoped limits carried by `limits[]`.
|
||||
*
|
||||
@@ -365,37 +400,6 @@ export async function fetchUsage(
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function readCredentials(): Promise<ClaudeCredentialRecord | null> {
|
||||
if (accessToken) return { oauth: { accessToken: accessToken } };
|
||||
if (configDir) return readCredentialFile(join(configDir, ".credentials.json"));
|
||||
const credPath = join(claudeHome, ".credentials.json");
|
||||
const fileCredentials = await readCredentialFile(credPath);
|
||||
return fileCredentials ?? (platform === "darwin" ? await readKeychainCredential() : null);
|
||||
}
|
||||
|
||||
async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord | null> {
|
||||
if (!existsSync(path)) return null;
|
||||
try {
|
||||
return toCredentialRecord(
|
||||
ClaudeCredentialsSchema.parse(JSON.parse(await fs.readFile(path, "utf8"))),
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function readKeychainCredential(): Promise<ClaudeCredentialRecord | null> {
|
||||
const parsed = ClaudeCredentialsSchema.safeParse(await readKeychainCredentials());
|
||||
return parsed.success ? toCredentialRecord(parsed.data) : null;
|
||||
}
|
||||
|
||||
function toCredentialRecord(
|
||||
credentials: z.infer<typeof ClaudeCredentialsSchema>,
|
||||
): ClaudeCredentialRecord | null {
|
||||
const oauth = credentials.claudeAiOauth;
|
||||
return oauth?.accessToken ? { oauth: { ...oauth, accessToken: oauth.accessToken } } : null;
|
||||
}
|
||||
|
||||
async function callClaudeApi(token: string): Promise<ClaudeUsageResponse | "NEEDS_AUTH"> {
|
||||
const res = await fetchApi("https://api.anthropic.com/api/oauth/usage", {
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
@@ -410,7 +414,7 @@ export async function fetchUsage(
|
||||
return ClaudeUsageResponseSchema.parse(await res.json());
|
||||
}
|
||||
|
||||
const credentials = await readCredentials();
|
||||
const credentials = await resolveClaudeCredentials(input, credentialLookup);
|
||||
if (!credentials) {
|
||||
return unavailableUsage();
|
||||
}
|
||||
@@ -463,10 +467,25 @@ const ClaudeAccountSchema = z.object({
|
||||
organizationUuid: z.string().min(1),
|
||||
emailAddress: z.string().optional(),
|
||||
});
|
||||
const profileCache = new Map<string, Promise<{ key: string; label?: string } | null>>();
|
||||
type ClaudeIdentity = { key: string; label?: string } | null;
|
||||
const PROFILE_TTL_MS = 300_000;
|
||||
const PROFILE_CACHE_LIMIT = 128;
|
||||
const profileCache = new Map<string, { at: number; result: Promise<ClaudeIdentity> }>();
|
||||
|
||||
export async function identify(input: UsageInput, fetchApi: typeof fetch = fetch) {
|
||||
const directory = "configDir" in input ? input.configDir : homedir();
|
||||
function pruneProfileCache(now: number): void {
|
||||
for (const [key, entry] of profileCache) {
|
||||
if (now - entry.at >= PROFILE_TTL_MS) profileCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export async function identify(
|
||||
input: UsageInput,
|
||||
fetchApi: typeof fetch = fetch,
|
||||
now: () => number = Date.now,
|
||||
credentialLookup: ClaudeCredentialLookup = {},
|
||||
) {
|
||||
const directory =
|
||||
"configDir" in input ? input.configDir : (credentialLookup.accountHome ?? homedir());
|
||||
if (!("accessToken" in input)) {
|
||||
try {
|
||||
const config = z
|
||||
@@ -478,13 +497,21 @@ export async function identify(input: UsageInput, fetchApi: typeof fetch = fetch
|
||||
...(account.emailAddress ? { label: account.emailAddress } : {}),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
// Credentials may still be present even when account metadata is absent.
|
||||
}
|
||||
}
|
||||
const token = input.accessToken;
|
||||
const credentials = await resolveClaudeCredentials(input, credentialLookup);
|
||||
if (!credentials) return null;
|
||||
const token = credentials.oauth.accessToken;
|
||||
const tokenHash = hashAccountKey(token);
|
||||
pruneProfileCache(now());
|
||||
const pending = profileCache.get(tokenHash);
|
||||
if (pending) return pending;
|
||||
if (pending) return pending.result;
|
||||
while (profileCache.size >= PROFILE_CACHE_LIMIT) {
|
||||
const oldest = profileCache.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
profileCache.delete(oldest);
|
||||
}
|
||||
const request = (async () => {
|
||||
const response = await fetchApi("https://api.anthropic.com/api/oauth/profile", {
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
@@ -508,11 +535,11 @@ export async function identify(input: UsageInput, fetchApi: typeof fetch = fetch
|
||||
...(account.email ? { label: account.email } : {}),
|
||||
};
|
||||
})();
|
||||
profileCache.set(tokenHash, request);
|
||||
profileCache.set(tokenHash, { at: now(), result: request });
|
||||
try {
|
||||
return await request;
|
||||
} catch (error) {
|
||||
profileCache.delete(tokenHash);
|
||||
if (profileCache.get(tokenHash)?.result === request) profileCache.delete(tokenHash);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ test("explicit codexHome reads only that auth file", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("explicit access token needs no auth file", async () => {
|
||||
test("identify returns a key when fetch finds Codex token credentials", async () => {
|
||||
await fetchUsage(
|
||||
{ accessToken: "fixture-supplied", accountId: "account-supplied" },
|
||||
(_url, init) => response(init?.headers ?? {}, "account-supplied"),
|
||||
@@ -142,3 +142,22 @@ test("token-only account claim survives token rotation", async () => {
|
||||
expect(await identify({ accessToken: token("second") })).toEqual({ key: "work-id" });
|
||||
expect(await identify({ accessToken: "opaque-token" })).toBeNull();
|
||||
});
|
||||
|
||||
test("identify reads an email label from auth id_token when access token has no profile", async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), "usage-codex-label-"));
|
||||
try {
|
||||
const idToken = `header.${Buffer.from(JSON.stringify({ email: "id-owner@example.test" })).toString("base64url")}.signature`;
|
||||
await writeFile(
|
||||
join(home, "auth.json"),
|
||||
JSON.stringify({
|
||||
tokens: { account_id: "account-id", access_token: "opaque-token", id_token: idToken },
|
||||
}),
|
||||
);
|
||||
expect(await identify({ codexHome: home })).toEqual({
|
||||
key: "account-id",
|
||||
label: "id-owner@example.test",
|
||||
});
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,7 +13,11 @@ import type { CodexUsageInput } from "../shared/input.js";
|
||||
|
||||
const authSchema = z.object({
|
||||
tokens: z
|
||||
.object({ access_token: z.string().optional(), account_id: z.string().optional() })
|
||||
.object({
|
||||
access_token: z.string().optional(),
|
||||
account_id: z.string().optional(),
|
||||
id_token: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
const number = z.coerce.number().finite();
|
||||
@@ -30,7 +34,7 @@ const responseSchema = z.object({
|
||||
|
||||
export async function readAuth(
|
||||
input: CodexUsageInput,
|
||||
): Promise<{ token: string; accountId?: string } | null> {
|
||||
): Promise<{ token: string; accountId?: string; idToken?: string } | null> {
|
||||
if ("accessToken" in input) return { token: input.accessToken, accountId: input.accountId };
|
||||
const candidates =
|
||||
"codexHome" in input
|
||||
@@ -44,7 +48,11 @@ export async function readAuth(
|
||||
try {
|
||||
const auth = authSchema.parse(JSON.parse(await readFile(path, "utf8")));
|
||||
if (auth.tokens?.access_token)
|
||||
return { token: auth.tokens.access_token, accountId: auth.tokens.account_id };
|
||||
return {
|
||||
token: auth.tokens.access_token,
|
||||
accountId: auth.tokens.account_id,
|
||||
idToken: auth.tokens.id_token,
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
@@ -118,33 +126,47 @@ export async function fetchUsage(
|
||||
};
|
||||
}
|
||||
|
||||
/** The account claim is decoded locally; the token itself never becomes identity. */
|
||||
/** JWT claims are decoded locally; no token or email becomes an account key. */
|
||||
function jwtClaims(token: string | undefined): Record<string, unknown> | null {
|
||||
try {
|
||||
const payload = token?.split(".")[1];
|
||||
return payload
|
||||
? (JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Record<string, unknown>)
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function claimObject(
|
||||
claims: Record<string, unknown> | null,
|
||||
name: string,
|
||||
): Record<string, unknown> | null {
|
||||
const value = claims?.[name];
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
function claimString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
export async function identify(input: CodexUsageInput) {
|
||||
const auth = await readAuth(input);
|
||||
if (!auth) return null;
|
||||
const claims = (() => {
|
||||
try {
|
||||
const payload = auth.token.split(".")[1];
|
||||
return payload
|
||||
? (JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
const nested = claims?.["https://api.openai.com/auth"];
|
||||
const claim =
|
||||
nested && typeof nested === "object"
|
||||
? (nested as Record<string, unknown>)["chatgpt_account_id"]
|
||||
: null;
|
||||
const access = jwtClaims(auth.token);
|
||||
const id = jwtClaims(auth.idToken);
|
||||
const accessAuth = claimObject(access, "https://api.openai.com/auth");
|
||||
const idAuth = claimObject(id, "https://api.openai.com/auth");
|
||||
const key =
|
||||
auth.accountId ??
|
||||
(typeof claim === "string" ? claim : null) ??
|
||||
(typeof claims?.["chatgpt_account_id"] === "string"
|
||||
? (claims["chatgpt_account_id"] as string)
|
||||
: null);
|
||||
return key ? { key } : null;
|
||||
claimString(accessAuth?.["chatgpt_account_id"]) ??
|
||||
claimString(access?.["chatgpt_account_id"]) ??
|
||||
claimString(idAuth?.["chatgpt_account_id"]);
|
||||
if (!key) return null;
|
||||
const label =
|
||||
claimString(claimObject(access, "https://api.openai.com/profile")?.["email"]) ??
|
||||
claimString(access?.["email"]) ??
|
||||
claimString(claimObject(id, "https://api.openai.com/profile")?.["email"]) ??
|
||||
claimString(id?.["email"]);
|
||||
return { key, ...(label ? { label } : {}) };
|
||||
}
|
||||
|
||||
@@ -112,10 +112,16 @@ describe("copilot usage source", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("identify uses the configured copilot account key", async () => {
|
||||
it("identify returns a key when fetch finds copilot credentials", async () => {
|
||||
const previous = process.env["COPILOT_TOKEN"];
|
||||
try {
|
||||
process.env["COPILOT_TOKEN"] = "fixture-token";
|
||||
let requested = false;
|
||||
await fetchUsage({}, async () => {
|
||||
requested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
});
|
||||
expect(requested).toBe(true);
|
||||
expect(await identify()).toEqual({ key: "default" });
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env["COPILOT_TOKEN"];
|
||||
|
||||
@@ -259,10 +259,16 @@ describe("cursor usage source", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("identify uses the configured cursor account key", async () => {
|
||||
it("identify returns a key when fetch finds cursor credentials", async () => {
|
||||
const previous = process.env["CURSOR_ACCESS_TOKEN"];
|
||||
try {
|
||||
process.env["CURSOR_ACCESS_TOKEN"] = "fixture-token";
|
||||
let requested = false;
|
||||
await fetchUsage({}, async () => {
|
||||
requested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
});
|
||||
expect(requested).toBe(true);
|
||||
expect(await identify()).toEqual({ key: "default" });
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env["CURSOR_ACCESS_TOKEN"];
|
||||
|
||||
@@ -267,10 +267,16 @@ describe("grok usage source", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("identify uses the configured grok account key", async () => {
|
||||
it("identify returns a key when fetch finds grok credentials", async () => {
|
||||
const previous = process.env["GROK_TOKEN"];
|
||||
try {
|
||||
process.env["GROK_TOKEN"] = "fixture-token";
|
||||
let requested = false;
|
||||
await fetchUsage({}, async () => {
|
||||
requested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
});
|
||||
expect(requested).toBe(true);
|
||||
expect(await identify()).toEqual({ key: "default" });
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env["GROK_TOKEN"];
|
||||
|
||||
@@ -369,10 +369,16 @@ describe("Kimi usage source usage windows", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("identify uses the configured kimi account key", async () => {
|
||||
it("identify returns a key when fetch finds kimi credentials", async () => {
|
||||
const previous = process.env["KIMI_TOKEN"];
|
||||
try {
|
||||
process.env["KIMI_TOKEN"] = "fixture-token";
|
||||
let requested = false;
|
||||
await fetchUsage({}, async () => {
|
||||
requested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
});
|
||||
expect(requested).toBe(true);
|
||||
expect(await identify()).toEqual({ key: "default" });
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env["KIMI_TOKEN"];
|
||||
|
||||
@@ -314,10 +314,16 @@ describe("minimax usage source", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("identify uses the configured minimax account key", async () => {
|
||||
it("identify returns a key when fetch finds minimax credentials", async () => {
|
||||
const previous = process.env["MINIMAX_API_KEY"];
|
||||
try {
|
||||
process.env["MINIMAX_API_KEY"] = "fixture-token";
|
||||
let requested = false;
|
||||
await fetchUsage({}, async () => {
|
||||
requested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
});
|
||||
expect(requested).toBe(true);
|
||||
expect(await identify()).toEqual({ key: "default" });
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env["MINIMAX_API_KEY"];
|
||||
|
||||
@@ -28,6 +28,7 @@ test("discovers and fetches the default key from read-only auth.json", async ()
|
||||
return Response.json(upstreamResponse);
|
||||
};
|
||||
const report = await fetchUsage({}, fetchApi as typeof fetch, path);
|
||||
expect((await identify({}, path))?.key).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(requests).toEqual([
|
||||
{ url: "https://opencode.ai/zen/go/v1/usage", authorization: "Bearer fixture-key" },
|
||||
]);
|
||||
@@ -75,7 +76,13 @@ test.each([401, 403])("maps HTTP %i to unavailable", async (status) => {
|
||||
expect(report.windows).toEqual([]);
|
||||
});
|
||||
|
||||
test("identifies OpenCode Go key without a usage request", async () => {
|
||||
test("identify returns a key when fetch finds OpenCode Go credentials", async () => {
|
||||
let requested = false;
|
||||
await fetchUsage({ apiKey: "fixture-secret" }, async () => {
|
||||
requested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
});
|
||||
expect(requested).toBe(true);
|
||||
const identity = await identify({ apiKey: "fixture-secret" });
|
||||
expect(identity?.key).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(identity?.key).not.toBe("fixture-secret");
|
||||
|
||||
@@ -118,10 +118,16 @@ describe("zai usage source", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("identify uses the configured zai account key", async () => {
|
||||
it("identify returns a key when fetch finds zai credentials", async () => {
|
||||
const previous = process.env["ZAI_API_KEY"];
|
||||
try {
|
||||
process.env["ZAI_API_KEY"] = "fixture-token";
|
||||
let requested = false;
|
||||
await fetchUsage({}, async () => {
|
||||
requested = true;
|
||||
return new Response(null, { status: 401 });
|
||||
});
|
||||
expect(requested).toBe(true);
|
||||
expect(await identify()).toEqual({ key: "default" });
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env["ZAI_API_KEY"];
|
||||
|
||||
Reference in New Issue
Block a user