@bitkyc08/opencodex 2.1.8 → 2.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from "node:fs";
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, chmodSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import * as z from "zod/v4";
4
5
  import type { OcxConfig } from "./types";
5
6
 
6
7
  let _atomicSeq = 0;
@@ -14,9 +15,38 @@ export function atomicWriteFile(path: string, content: string): void {
14
15
  renameSync(tmp, path);
15
16
  }
16
17
 
17
- const OCX_DIR = join(homedir(), ".opencodex");
18
- const CONFIG_PATH = join(OCX_DIR, "config.json");
19
- const PID_PATH = join(OCX_DIR, "ocx.pid");
18
+ function resolveConfigDir(): string {
19
+ return process.env["OPENCODEX_HOME"] || join(homedir(), ".opencodex");
20
+ }
21
+
22
+ function resolveConfigPath(): string {
23
+ return join(resolveConfigDir(), "config.json");
24
+ }
25
+
26
+ function resolvePidPath(): string {
27
+ return join(resolveConfigDir(), "ocx.pid");
28
+ }
29
+
30
+ const warnedConfigFallbacks = new Set<string>();
31
+
32
+ const providerConfigSchema = z.object({
33
+ adapter: z.string().min(1),
34
+ baseUrl: z.string().min(1),
35
+ }).passthrough();
36
+
37
+ const configSchema = z.object({
38
+ port: z.number().int().min(0).max(65535).default(10100),
39
+ providers: z.record(z.string(), providerConfigSchema),
40
+ defaultProvider: z.string().min(1),
41
+ }).passthrough().superRefine((config, ctx) => {
42
+ if (Object.keys(config.providers).length > 0 && !(config.defaultProvider in config.providers)) {
43
+ ctx.addIssue({
44
+ code: "custom",
45
+ path: ["defaultProvider"],
46
+ message: "defaultProvider must exist in providers",
47
+ });
48
+ }
49
+ });
20
50
 
21
51
  /**
22
52
  * Default featured subagent models (native GPT) seeded on a fresh install and when `subagentModels`
@@ -28,20 +58,21 @@ const PID_PATH = join(OCX_DIR, "ocx.pid");
28
58
  export const DEFAULT_SUBAGENT_MODELS = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark"];
29
59
 
30
60
  export function getConfigDir(): string {
31
- return OCX_DIR;
61
+ return resolveConfigDir();
32
62
  }
33
63
 
34
64
  export function getConfigPath(): string {
35
- return CONFIG_PATH;
65
+ return resolveConfigPath();
36
66
  }
37
67
 
38
68
  export function getPidPath(): string {
39
- return PID_PATH;
69
+ return resolvePidPath();
40
70
  }
41
71
 
42
72
  export function hardenConfigDir(): void {
43
- if (existsSync(OCX_DIR)) {
44
- try { chmodSync(OCX_DIR, 0o700); } catch { /* best-effort */ }
73
+ const dir = getConfigDir();
74
+ if (existsSync(dir)) {
75
+ try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
45
76
  }
46
77
  }
47
78
 
@@ -52,27 +83,31 @@ export function hardenExistingSecret(path: string): void {
52
83
  }
53
84
 
54
85
  export function loadConfig(): OcxConfig {
86
+ const dir = getConfigDir();
87
+ const configPath = getConfigPath();
55
88
  hardenConfigDir();
56
- hardenExistingSecret(CONFIG_PATH);
57
- hardenExistingSecret(join(OCX_DIR, "auth.json"));
58
- if (!existsSync(CONFIG_PATH)) {
89
+ hardenExistingSecret(configPath);
90
+ hardenExistingSecret(join(dir, "auth.json"));
91
+ if (!existsSync(configPath)) {
59
92
  return getDefaultConfig();
60
93
  }
61
94
  try {
62
- const raw = readFileSync(CONFIG_PATH, "utf-8");
63
- return JSON.parse(raw) as OcxConfig;
64
- } catch {
95
+ const raw = readFileSync(configPath, "utf-8");
96
+ return configSchema.parse(JSON.parse(raw)) as OcxConfig;
97
+ } catch (error) {
98
+ warnAndBackupInvalidConfig(configPath, error);
65
99
  return getDefaultConfig();
66
100
  }
67
101
  }
68
102
 
69
103
  export function saveConfig(config: OcxConfig): void {
70
- if (!existsSync(OCX_DIR)) {
71
- mkdirSync(OCX_DIR, { recursive: true, mode: 0o700 });
104
+ const dir = getConfigDir();
105
+ if (!existsSync(dir)) {
106
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
72
107
  } else {
73
- try { chmodSync(OCX_DIR, 0o700); } catch { /* best-effort on existing dir */ }
108
+ try { chmodSync(dir, 0o700); } catch { /* best-effort on existing dir */ }
74
109
  }
75
- atomicWriteFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
110
+ atomicWriteFile(getConfigPath(), JSON.stringify(config, null, 2) + "\n");
76
111
  }
77
112
 
78
113
  export function websocketsEnabled(config: Pick<OcxConfig, "websockets">): boolean {
@@ -112,18 +147,20 @@ export function resolveEnvValue(value: string | undefined): string | undefined {
112
147
  }
113
148
 
114
149
  export function writePid(pid: number): void {
115
- if (!existsSync(OCX_DIR)) {
116
- mkdirSync(OCX_DIR, { recursive: true, mode: 0o700 });
150
+ const dir = getConfigDir();
151
+ if (!existsSync(dir)) {
152
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
117
153
  } else {
118
154
  hardenConfigDir();
119
155
  }
120
- writeFileSync(PID_PATH, String(pid), "utf-8");
156
+ writeFileSync(getPidPath(), String(pid), "utf-8");
121
157
  }
122
158
 
123
159
  export function readPid(): number | null {
124
- if (!existsSync(PID_PATH)) return null;
160
+ const pidPath = getPidPath();
161
+ if (!existsSync(pidPath)) return null;
125
162
  try {
126
- const raw = readFileSync(PID_PATH, "utf-8").trim();
163
+ const raw = readFileSync(pidPath, "utf-8").trim();
127
164
  const pid = parseInt(raw, 10);
128
165
  if (isNaN(pid)) return null;
129
166
  try {
@@ -140,7 +177,30 @@ export function readPid(): number | null {
140
177
 
141
178
  export function removePid(): void {
142
179
  try {
143
- const { unlinkSync } = require("node:fs");
144
- unlinkSync(PID_PATH);
180
+ unlinkSync(getPidPath());
145
181
  } catch { /* ignore */ }
146
182
  }
183
+
184
+ function warnAndBackupInvalidConfig(configPath: string, error: unknown): void {
185
+ if (warnedConfigFallbacks.has(configPath)) return;
186
+ warnedConfigFallbacks.add(configPath);
187
+
188
+ const backupPath = backupInvalidConfig(configPath);
189
+ const reason = error instanceof z.ZodError
190
+ ? error.issues.map(issue => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ")
191
+ : error instanceof Error ? error.message : String(error);
192
+ const backupNote = backupPath ? ` A backup was written to ${backupPath}.` : "";
193
+ console.error(`Could not load opencodex config at ${configPath}: ${reason}. Using default config.${backupNote}`);
194
+ }
195
+
196
+ function backupInvalidConfig(configPath: string): string | null {
197
+ if (!existsSync(configPath)) return null;
198
+ const backupPath = `${configPath}.invalid-${new Date().toISOString().replace(/[:.]/g, "-")}`;
199
+ try {
200
+ copyFileSync(configPath, backupPath);
201
+ try { chmodSync(backupPath, 0o600); } catch { /* best-effort */ }
202
+ return backupPath;
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
package/src/debug.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  // Opt-in frame-drop visibility. The streaming path is intentionally quiet (no unconditional
2
2
  // console output), so this no-ops unless OCX_DEBUG_FRAMES=1. Lets a malformed/chunk-split
3
3
  // upstream frame be detected instead of silently truncating content.
4
- const DEBUG_FRAMES = process.env.OCX_DEBUG_FRAMES === "1";
4
+ function debugFramesEnabled(): boolean {
5
+ return process.env.OCX_DEBUG_FRAMES === "1";
6
+ }
5
7
 
6
8
  export function debugDroppedFrame(adapter: string, payload: string): void {
7
- if (!DEBUG_FRAMES) return;
8
- const preview = payload.length > 200 ? `${payload.slice(0, 200)}…` : payload;
9
- console.error(`[ocx:frame-drop] ${adapter}: ${preview}`);
9
+ if (!debugFramesEnabled()) return;
10
+ console.error(`[ocx:frame-drop] ${adapter}: dropped malformed upstream frame (payload redacted, bytes=${payload.length})`);
10
11
  }
@@ -0,0 +1,150 @@
1
+ import { OAuthCallbackFlow } from "./callback-server";
2
+ import type { OAuthController, OAuthCredentials } from "./types";
3
+ import { generatePKCE } from "./pkce";
4
+
5
+ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
6
+ const AUTH_URL = "https://auth.openai.com/oauth/authorize";
7
+ const TOKEN_URL = "https://auth.openai.com/oauth/token";
8
+ const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
9
+ const CALLBACK_PORT = 1455;
10
+ const CALLBACK_PATH = "/auth/callback";
11
+ const ORIGINATOR = "opencodex";
12
+
13
+ export function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
14
+ const parts = token.split(".");
15
+ if (parts.length !== 3 || !parts[1]) return undefined;
16
+ try {
17
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as Record<string, unknown>;
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ export function extractAccountId(idToken?: string, accessToken?: string): string | undefined {
24
+ for (const token of [idToken, accessToken]) {
25
+ if (!token) continue;
26
+ const payload = decodeJwtPayload(token);
27
+ if (!payload) continue;
28
+ if (typeof payload.chatgpt_account_id === "string") return payload.chatgpt_account_id;
29
+ const ns = payload["https://api.openai.com/auth"];
30
+ if (ns && typeof ns === "object" && typeof (ns as Record<string, unknown>).chatgpt_account_id === "string") {
31
+ return (ns as Record<string, unknown>).chatgpt_account_id as string;
32
+ }
33
+ const orgs = payload.organizations;
34
+ if (Array.isArray(orgs) && orgs[0] && typeof orgs[0].id === "string") return orgs[0].id as string;
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ export function extractEmail(idToken?: string, accessToken?: string): string | undefined {
40
+ for (const token of [idToken, accessToken]) {
41
+ if (!token) continue;
42
+ const payload = decodeJwtPayload(token);
43
+ if (!payload) continue;
44
+ if (typeof payload.email === "string") return payload.email.toLowerCase();
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ function credsFromToken(data: Record<string, unknown>): OAuthCredentials {
50
+ const idToken = typeof data.id_token === "string" ? data.id_token : undefined;
51
+ const accessToken = data.access_token as string;
52
+ return {
53
+ access: accessToken,
54
+ refresh: (data.refresh_token as string) ?? "",
55
+ expires: Date.now() + ((data.expires_in as number) ?? 3600) * 1000,
56
+ accountId: extractAccountId(idToken, accessToken),
57
+ email: extractEmail(idToken, accessToken),
58
+ };
59
+ }
60
+
61
+ export class ChatGPTOAuthFlow extends OAuthCallbackFlow {
62
+ #verifier = "";
63
+ forceLogin = false;
64
+
65
+ constructor(ctrl: OAuthController) {
66
+ super(ctrl, {
67
+ preferredPort: CALLBACK_PORT,
68
+ callbackPath: CALLBACK_PATH,
69
+ callbackHostname: "localhost",
70
+ callbackBindHostname: "127.0.0.1",
71
+ redirectUri: `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`,
72
+ });
73
+ }
74
+
75
+ async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
76
+ const pkce = await generatePKCE();
77
+ this.#verifier = pkce.verifier;
78
+ const params = new URLSearchParams({
79
+ response_type: "code",
80
+ client_id: CLIENT_ID,
81
+ redirect_uri: redirectUri,
82
+ scope: SCOPE,
83
+ code_challenge: pkce.challenge,
84
+ code_challenge_method: "S256",
85
+ state,
86
+ codex_cli_simplified_flow: "true",
87
+ originator: ORIGINATOR,
88
+ });
89
+ params.set("id_token_add_organizations", "true");
90
+ if (this.forceLogin) params.set("prompt", "login");
91
+ return {
92
+ url: `${AUTH_URL}?${params}`,
93
+ instructions: "Complete ChatGPT login in your browser.",
94
+ };
95
+ }
96
+
97
+ async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
98
+ if (!this.#verifier) throw new Error("ChatGPT PKCE verifier not initialized");
99
+ const resp = await fetch(TOKEN_URL, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
102
+ body: new URLSearchParams({
103
+ grant_type: "authorization_code",
104
+ client_id: CLIENT_ID,
105
+ code,
106
+ redirect_uri: redirectUri,
107
+ code_verifier: this.#verifier,
108
+ }).toString(),
109
+ });
110
+ if (!resp.ok) {
111
+ const errDesc = await safeErrorDescription(resp);
112
+ throw new Error(`ChatGPT token exchange failed: ${resp.status} ${errDesc}`);
113
+ }
114
+ return credsFromToken((await resp.json()) as Record<string, unknown>);
115
+ }
116
+ }
117
+
118
+ function safeErrorDescription(resp: Response): Promise<string> {
119
+ return resp.text().catch(() => "").then(text => {
120
+ try {
121
+ const parsed = JSON.parse(text) as { error?: string; error_description?: string };
122
+ return [parsed.error, parsed.error_description].filter(Boolean).join(": ") || `HTTP ${resp.status}`;
123
+ } catch { return `HTTP ${resp.status}`; }
124
+ });
125
+ }
126
+
127
+ export async function loginChatGPT(ctrl: OAuthController, opts?: { forceLogin?: boolean }): Promise<OAuthCredentials> {
128
+ const flow = new ChatGPTOAuthFlow(ctrl);
129
+ if (opts?.forceLogin) flow.forceLogin = true;
130
+ return flow.login();
131
+ }
132
+
133
+ // Note: uses form-urlencoded per OAuth 2.0 spec (RFC 6749 §6).
134
+ // Codex-rs uses JSON for refresh — intentional divergence; both accepted by auth.openai.com.
135
+ export async function refreshChatGPTToken(refreshToken: string): Promise<OAuthCredentials> {
136
+ const resp = await fetch(TOKEN_URL, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
139
+ body: new URLSearchParams({
140
+ grant_type: "refresh_token",
141
+ client_id: CLIENT_ID,
142
+ refresh_token: refreshToken,
143
+ }).toString(),
144
+ });
145
+ if (!resp.ok) {
146
+ const errDesc = await safeErrorDescription(resp);
147
+ throw new Error(`ChatGPT refresh failed: ${resp.status} ${errDesc}`);
148
+ }
149
+ return credsFromToken((await resp.json()) as Record<string, unknown>);
150
+ }
@@ -1,16 +1,20 @@
1
1
  import type { OAuthController, OAuthCredentials } from "./types";
2
2
  import type { OcxConfig, OcxProviderConfig } from "../types";
3
3
  import { loadConfig, resolveEnvValue, saveConfig } from "../config";
4
+ import { maskEmail } from "../privacy";
4
5
  import { getCredential, saveCredential } from "./store";
5
6
  import { loginXai, refreshXaiToken } from "./xai";
6
7
  import { ANTHROPIC_OAUTH_BETA, loginAnthropic, refreshAnthropicToken } from "./anthropic";
7
8
  import { loginKimi, refreshKimiToken } from "./kimi";
9
+ import { loginChatGPT, refreshChatGPTToken } from "./chatgpt";
8
10
  import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
9
11
 
10
12
  const REFRESH_SKEW_MS = 60_000;
11
13
 
14
+ export interface LoginOpts { forceLogin?: boolean }
15
+
12
16
  interface OAuthProviderDef {
13
- login(ctrl: OAuthController): Promise<OAuthCredentials>;
17
+ login(ctrl: OAuthController, opts?: LoginOpts): Promise<OAuthCredentials>;
14
18
  refresh(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredentials>;
15
19
  /** provider entry written into config.json on first login. */
16
20
  providerConfig: OcxProviderConfig;
@@ -48,6 +52,12 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
48
52
  providerConfig: oauthConfig("kimi"),
49
53
  defaultModel: oauthDefaultModel("kimi"),
50
54
  },
55
+ chatgpt: {
56
+ login: loginChatGPT,
57
+ refresh: (rt) => refreshChatGPTToken(rt),
58
+ providerConfig: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const },
59
+ defaultModel: "gpt-5.4",
60
+ },
51
61
  };
52
62
 
53
63
  export function isOAuthProvider(name: string): boolean {
@@ -178,10 +188,10 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
178
188
  }
179
189
 
180
190
  /** Run the login flow, persist the credential + upsert the provider entry to disk, return cred. */
181
- export async function runLogin(provider: string, ctrl: OAuthController): Promise<OAuthCredentials> {
191
+ export async function runLogin(provider: string, ctrl: OAuthController, opts?: LoginOpts): Promise<OAuthCredentials> {
182
192
  const def = OAUTH_PROVIDERS[provider];
183
193
  if (!def) throw new Error(`Unknown OAuth provider: ${provider}`);
184
- const cred = await def.login(ctrl);
194
+ const cred = await def.login(ctrl, opts);
185
195
  saveCredential(provider, cred);
186
196
  const config = loadConfig();
187
197
  upsertOAuthProvider(config, provider);
@@ -195,18 +205,31 @@ export async function runLogin(provider: string, ctrl: OAuthController): Promise
195
205
  * error surfaced via getLoginStatus().
196
206
  */
197
207
  const loginState = new Map<string, { error?: string; done: boolean }>();
208
+ const loginAbort = new Map<string, AbortController>();
198
209
 
199
- export function getLoginStatus(provider: string): { loggedIn: boolean; email?: string; error?: string } {
210
+ export function getLoginStatus(provider: string): { loggedIn: boolean; email?: string; error?: string; done: boolean } {
200
211
  const cred = getCredential(provider);
201
212
  const st = loginState.get(provider);
202
- return { loggedIn: !!cred, email: cred?.email, error: st?.error };
213
+ return { loggedIn: !!cred, email: maskEmail(cred?.email) ?? undefined, error: st?.error, done: st?.done ?? false };
203
214
  }
204
215
 
205
216
  export function clearLoginState(provider: string): void {
217
+ loginAbort.get(provider)?.abort("cleared");
218
+ loginAbort.delete(provider);
206
219
  loginState.delete(provider);
207
220
  }
208
221
 
209
- export async function startLoginFlow(provider: string): Promise<{ url: string; instructions?: string }> {
222
+ export function cancelLoginFlow(provider: string): boolean {
223
+ const ctrl = loginAbort.get(provider);
224
+ const existing = loginState.get(provider);
225
+ if (!ctrl && (!existing || existing.done)) return false;
226
+ ctrl?.abort("cancelled");
227
+ loginAbort.delete(provider);
228
+ loginState.set(provider, { done: true, error: "Login cancelled" });
229
+ return true;
230
+ }
231
+
232
+ export async function startLoginFlow(provider: string, opts?: LoginOpts): Promise<{ url: string; instructions?: string }> {
210
233
  const def = OAUTH_PROVIDERS[provider];
211
234
  if (!def) throw new Error(`Unknown OAuth provider: ${provider}`);
212
235
  const existing = loginState.get(provider);
@@ -214,6 +237,8 @@ export async function startLoginFlow(provider: string): Promise<{ url: string; i
214
237
  throw new Error(`A login for ${provider} is already in progress`);
215
238
  }
216
239
  loginState.set(provider, { done: false });
240
+ const abort = new AbortController();
241
+ loginAbort.set(provider, abort);
217
242
  return new Promise((resolve, reject) => {
218
243
  let urlResolved = false;
219
244
  const ctrl: OAuthController = {
@@ -222,16 +247,19 @@ export async function startLoginFlow(provider: string): Promise<{ url: string; i
222
247
  resolve({ url, instructions });
223
248
  },
224
249
  onProgress: () => {},
250
+ signal: abort.signal,
225
251
  };
226
252
  // Background: runLogin persists the credential + upserts the provider entry to disk config.
227
- runLogin(provider, ctrl)
253
+ runLogin(provider, ctrl, opts)
228
254
  .then(() => {
255
+ loginAbort.delete(provider);
229
256
  loginState.set(provider, { done: true });
230
257
  // Local-token import (grok-cli / Claude Code keychain) completes WITHOUT firing onAuth —
231
258
  // resolve so the GUI call returns instead of hanging.
232
259
  if (!urlResolved) resolve({ url: "", instructions: "Logged in via an existing local CLI/keychain token — no browser needed." });
233
260
  })
234
261
  .catch((e: unknown) => {
262
+ loginAbort.delete(provider);
235
263
  const msg = e instanceof Error ? e.message : String(e);
236
264
  loginState.set(provider, { done: true, error: msg });
237
265
  if (!urlResolved) reject(e);
@@ -4,15 +4,19 @@ import { join } from "node:path";
4
4
  import { getConfigDir, atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "../config";
5
5
  import type { OAuthCredentials } from "./types";
6
6
 
7
- const AUTH_PATH = join(getConfigDir(), "auth.json");
8
7
  type AuthStore = Record<string, OAuthCredentials>;
9
8
 
9
+ function authPath(): string {
10
+ return join(getConfigDir(), "auth.json");
11
+ }
12
+
10
13
  export function loadAuthStore(): AuthStore {
14
+ const path = authPath();
11
15
  hardenConfigDir();
12
- hardenExistingSecret(AUTH_PATH);
13
- if (!existsSync(AUTH_PATH)) return {};
16
+ hardenExistingSecret(path);
17
+ if (!existsSync(path)) return {};
14
18
  try {
15
- return JSON.parse(readFileSync(AUTH_PATH, "utf-8")) as AuthStore;
19
+ return JSON.parse(readFileSync(path, "utf-8")) as AuthStore;
16
20
  } catch {
17
21
  return {};
18
22
  }
@@ -25,7 +29,7 @@ function persist(store: AuthStore): void {
25
29
  } else {
26
30
  try { chmodSync(dir, 0o700); } catch { /* best-effort on existing dir */ }
27
31
  }
28
- atomicWriteFile(AUTH_PATH, JSON.stringify(store, null, 2) + "\n");
32
+ atomicWriteFile(authPath(), JSON.stringify(store, null, 2) + "\n");
29
33
  }
30
34
 
31
35
  export function getCredential(provider: string): OAuthCredentials | null {
package/src/privacy.ts ADDED
@@ -0,0 +1,11 @@
1
+ export function maskEmail(value: string | null | undefined): string | null {
2
+ if (!value) return null;
3
+ const at = value.indexOf("@");
4
+ if (at <= 0) return value;
5
+ const local = value.slice(0, at);
6
+ const domain = value.slice(at + 1);
7
+ if (!domain) return value;
8
+ if (local.length === 1) return `*@${domain}`;
9
+ if (local.length === 2) return `${local[0]}*@${domain}`;
10
+ return `${local[0]}***${local[local.length - 1]}@${domain}`;
11
+ }
package/src/router.ts CHANGED
@@ -11,7 +11,7 @@ const MODEL_PROVIDER_PATTERNS: Record<string, string[]> = {
11
11
  anthropic: [
12
12
  "claude-", "claude-sonnet-", "claude-opus-", "claude-haiku-",
13
13
  ],
14
- openai: [
14
+ chatgpt: [
15
15
  "gpt-", "o1-", "o3-", "o4-",
16
16
  ],
17
17
  groq: [