
























@@ -0,0 +1,177 @@
1+import fs from "node:fs/promises";
2+import path from "node:path";
3+4+export const QA_CODEX_OAUTH_PROFILE_ID = "openai-codex:qa-oauth";
5+export const QA_OPENAI_API_KEY_PROFILE_ID = "openai:media-api";
6+export const QA_AUTH_PROFILE_STORE_VERSION = 1;
7+8+export type QaAuthProfileShape = "oauth-only" | "apikey-only" | "mixed";
9+10+export type QaApiKeyAuthProfile = {
11+type: "api_key";
12+provider: "openai";
13+key: string;
14+displayName: string;
15+};
16+17+export type QaOAuthAuthProfile = {
18+type: "oauth";
19+provider: "openai-codex";
20+access: string;
21+refresh: string;
22+expires: number;
23+email: string;
24+displayName: string;
25+};
26+27+export type QaAuthProfile = QaApiKeyAuthProfile | QaOAuthAuthProfile;
28+29+export type QaAuthProfileSnapshot = {
30+version: number;
31+profiles: Record<string, QaAuthProfile>;
32+};
33+34+export type QaCodexAuthProfileSelection =
35+| {
36+status: "ready";
37+profileId: string;
38+provider: "openai-codex";
39+mode: "oauth";
40+}
41+| {
42+status: "blocked";
43+remediation: string;
44+};
45+46+const QA_FIXED_OAUTH_EXPIRY_MS = Date.UTC(2036, 0, 1);
47+48+function authProfilesPath(agentDir: string) {
49+return path.join(agentDir, "auth-profiles.json");
50+}
51+52+function buildCodexOAuthProfile(): QaOAuthAuthProfile {
53+return {
54+type: "oauth",
55+provider: "openai-codex",
56+access: "qa-codex-oauth-access-placeholder",
57+refresh: "qa-codex-oauth-refresh-placeholder",
58+expires: QA_FIXED_OAUTH_EXPIRY_MS,
59+email: "qa-codex@example.test",
60+displayName: "QA Codex OAuth profile",
61+};
62+}
63+64+function buildOpenAiApiKeyProfile(): QaApiKeyAuthProfile {
65+return {
66+type: "api_key",
67+provider: "openai",
68+key: "qa-openai-not-a-real-key",
69+displayName: "QA OpenAI API-key profile",
70+};
71+}
72+73+function buildProfileMap(shape: QaAuthProfileShape): Record<string, QaAuthProfile> {
74+switch (shape) {
75+case "oauth-only":
76+return {
77+[QA_CODEX_OAUTH_PROFILE_ID]: buildCodexOAuthProfile(),
78+};
79+case "apikey-only":
80+return {
81+[QA_OPENAI_API_KEY_PROFILE_ID]: buildOpenAiApiKeyProfile(),
82+};
83+case "mixed":
84+return {
85+[QA_CODEX_OAUTH_PROFILE_ID]: buildCodexOAuthProfile(),
86+[QA_OPENAI_API_KEY_PROFILE_ID]: buildOpenAiApiKeyProfile(),
87+};
88+}
89+const exhaustive: never = shape;
90+return exhaustive;
91+}
92+93+function isQaAuthProfile(value: unknown): value is QaAuthProfile {
94+if (!value || typeof value !== "object") {
95+return false;
96+}
97+const record = value as Record<string, unknown>;
98+return (
99+(record.type === "oauth" && record.provider === "openai-codex") ||
100+(record.type === "api_key" && record.provider === "openai")
101+);
102+}
103+104+function normalizeAuthProfileSnapshot(value: unknown): QaAuthProfileSnapshot {
105+if (!value || typeof value !== "object") {
106+return { version: QA_AUTH_PROFILE_STORE_VERSION, profiles: {} };
107+}
108+const record = value as Record<string, unknown>;
109+const profilesRecord =
110+record.profiles && typeof record.profiles === "object"
111+ ? (record.profiles as Record<string, unknown>)
112+ : {};
113+const profiles = Object.fromEntries(
114+Object.entries(profilesRecord)
115+.filter((entry): entry is [string, QaAuthProfile] => isQaAuthProfile(entry[1]))
116+.toSorted(([left], [right]) => left.localeCompare(right)),
117+);
118+return {
119+version:
120+typeof record.version === "number" && Number.isFinite(record.version)
121+ ? record.version
122+ : QA_AUTH_PROFILE_STORE_VERSION,
123+ profiles,
124+};
125+}
126+127+export async function seedAuthProfiles(
128+shape: QaAuthProfileShape,
129+agentDir: string,
130+): Promise<QaAuthProfileSnapshot> {
131+const snapshot = {
132+version: QA_AUTH_PROFILE_STORE_VERSION,
133+profiles: buildProfileMap(shape),
134+};
135+await fs.mkdir(agentDir, { recursive: true });
136+await fs.writeFile(authProfilesPath(agentDir), `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
137+return snapshot;
138+}
139+140+export async function snapshotAuthProfiles(agentDir: string): Promise<QaAuthProfileSnapshot> {
141+const raw = await fs.readFile(authProfilesPath(agentDir), "utf8").catch((error: unknown) => {
142+if (error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") {
143+return null;
144+}
145+throw error;
146+});
147+if (!raw) {
148+return { version: QA_AUTH_PROFILE_STORE_VERSION, profiles: {} };
149+}
150+return normalizeAuthProfileSnapshot(JSON.parse(raw) as unknown);
151+}
152+153+export function resolveCodexAuthProfile(
154+snapshot: QaAuthProfileSnapshot,
155+): QaCodexAuthProfileSelection {
156+const profileId = Object.keys(snapshot.profiles)
157+.toSorted((left, right) => left.localeCompare(right))
158+.find((candidate) => {
159+const profile = snapshot.profiles[candidate];
160+return profile?.type === "oauth" && profile.provider === "openai-codex";
161+});
162+163+if (!profileId) {
164+return {
165+status: "blocked",
166+remediation:
167+'Codex app-server auth requires an openai-codex OAuth profile. Run "openclaw doctor --fix" to repair Codex auth routing before retrying.',
168+};
169+}
170+171+return {
172+status: "ready",
173+ profileId,
174+provider: "openai-codex",
175+mode: "oauth",
176+};
177+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。