[codex] fix cross-database JSON boundary handling (#283)
* fix: normalize cross-database json boundaries * fix: address json boundary review comments --------- Co-authored-by: apple <apple@appledeMacBook-Pro.local>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildCodexOauthInfo,
|
||||
getCodexOauthInfoFromExtraConfig,
|
||||
isCodexPlatform,
|
||||
} from './codexAccount.js';
|
||||
|
||||
describe('codexAccount', () => {
|
||||
it('reads codex oauth info from parsed extra config objects', () => {
|
||||
const extraConfig = {
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountKey: 'chatgpt-account-123',
|
||||
refreshToken: 'refresh-token',
|
||||
},
|
||||
};
|
||||
|
||||
expect(getCodexOauthInfoFromExtraConfig(extraConfig)).toEqual(expect.objectContaining({
|
||||
provider: 'codex',
|
||||
accountId: 'chatgpt-account-123',
|
||||
accountKey: 'chatgpt-account-123',
|
||||
refreshToken: 'refresh-token',
|
||||
}));
|
||||
});
|
||||
|
||||
it('recognizes parsed codex extra config objects as codex platform accounts', () => {
|
||||
expect(isCodexPlatform({
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountKey: 'chatgpt-account-123',
|
||||
},
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('builds codex oauth info from parsed extra config objects', () => {
|
||||
const oauth = buildCodexOauthInfo({
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountKey: 'chatgpt-account-123',
|
||||
refreshToken: 'refresh-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(oauth).toEqual(expect.objectContaining({
|
||||
provider: 'codex',
|
||||
accountId: 'chatgpt-account-123',
|
||||
accountKey: 'chatgpt-account-123',
|
||||
refreshToken: 'refresh-token',
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -10,21 +10,47 @@ export type CodexOauthInfo = OauthInfo & {
|
||||
provider: 'codex';
|
||||
};
|
||||
|
||||
export function getCodexOauthInfoFromExtraConfig(extraConfig?: string | null): CodexOauthInfo | null {
|
||||
type OauthExtraConfigInput = Parameters<typeof getOauthInfoFromExtraConfig>[0];
|
||||
type BuildOauthInfoInput = Parameters<typeof buildOauthInfo>[0];
|
||||
type OauthIdentityCarrierLike = Pick<
|
||||
typeof schema.accounts.$inferSelect,
|
||||
'extraConfig' | 'oauthProvider' | 'oauthAccountKey' | 'oauthProjectId'
|
||||
>;
|
||||
|
||||
function isOauthIdentityCarrierLike(value: unknown): value is OauthIdentityCarrierLike {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
return 'extraConfig' in value
|
||||
|| 'oauthProvider' in value
|
||||
|| 'oauthAccountKey' in value
|
||||
|| 'oauthProjectId' in value;
|
||||
}
|
||||
|
||||
export function getCodexOauthInfoFromExtraConfig(extraConfig?: OauthExtraConfigInput): CodexOauthInfo | null {
|
||||
const oauth = getOauthInfoFromExtraConfig(extraConfig);
|
||||
if (!oauth || oauth.provider !== 'codex') return null;
|
||||
return oauth as CodexOauthInfo;
|
||||
}
|
||||
|
||||
export function isCodexPlatform(
|
||||
account: Pick<typeof schema.accounts.$inferSelect, 'extraConfig'> | string | null | undefined,
|
||||
account: OauthIdentityCarrierLike | OauthExtraConfigInput,
|
||||
): boolean {
|
||||
return isOauthProvider(account, 'codex');
|
||||
if (!account || typeof account === 'string') {
|
||||
return isOauthProvider(account, 'codex');
|
||||
}
|
||||
if (isOauthIdentityCarrierLike(account)) {
|
||||
return isOauthProvider(account, 'codex');
|
||||
}
|
||||
return getCodexOauthInfoFromExtraConfig(account) !== null;
|
||||
}
|
||||
|
||||
export function buildCodexOauthInfo(
|
||||
extraConfig?: string | null,
|
||||
extraConfig?: BuildOauthInfoInput,
|
||||
patch: Partial<CodexOauthInfo> = {},
|
||||
): CodexOauthInfo {
|
||||
return buildOauthInfo(extraConfig, { provider: 'codex', ...patch }) as CodexOauthInfo;
|
||||
}
|
||||
|
||||
export type {
|
||||
OauthExtraConfigInput,
|
||||
OauthIdentityCarrierLike,
|
||||
};
|
||||
|
||||
@@ -58,6 +58,30 @@ describe('oauth account identity helpers', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('reads oauth metadata when extraConfig is already a parsed object', () => {
|
||||
const oauth = getOauthInfoFromAccount({
|
||||
oauthProvider: null,
|
||||
oauthAccountKey: null,
|
||||
oauthProjectId: null,
|
||||
extraConfig: {
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountKey: 'json-user',
|
||||
projectId: 'json-project',
|
||||
refreshToken: 'refresh-token',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(oauth).toEqual(expect.objectContaining({
|
||||
provider: 'codex',
|
||||
accountId: 'json-user',
|
||||
accountKey: 'json-user',
|
||||
projectId: 'json-project',
|
||||
refreshToken: 'refresh-token',
|
||||
}));
|
||||
});
|
||||
|
||||
it('reconstructs oauth runtime state from extraConfig even when identity fields are stripped', () => {
|
||||
const oauth = getOauthInfoFromAccount({
|
||||
oauthProvider: 'gemini-cli',
|
||||
|
||||
@@ -23,6 +23,8 @@ type ParsedExtraConfig = {
|
||||
oauth?: ParsedOauthInfo;
|
||||
};
|
||||
|
||||
type ExtraConfigInput = string | Record<string, unknown> | null | undefined;
|
||||
|
||||
export type OauthModelDiscoveryStatus = 'healthy' | 'abnormal';
|
||||
|
||||
export type OauthInfo = {
|
||||
@@ -46,7 +48,7 @@ export type OauthInfo = {
|
||||
export type StoredOauthState = Omit<OauthInfo, 'provider' | 'accountId' | 'accountKey' | 'projectId'>;
|
||||
|
||||
type OauthIdentityCarrier = {
|
||||
extraConfig?: string | null;
|
||||
extraConfig?: ExtraConfigInput;
|
||||
oauthProvider?: string | null;
|
||||
oauthAccountKey?: string | null;
|
||||
oauthProjectId?: string | null;
|
||||
@@ -54,11 +56,17 @@ type OauthIdentityCarrier = {
|
||||
|
||||
type StoredOauthIdentity = Pick<OauthInfo, 'provider' | 'accountId' | 'accountKey' | 'projectId'>;
|
||||
|
||||
function parseExtraConfig(extraConfig?: string | null): ParsedExtraConfig {
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function parseExtraConfig(extraConfig?: ExtraConfigInput): ParsedExtraConfig {
|
||||
if (!extraConfig) return {};
|
||||
if (isRecord(extraConfig)) return extraConfig as ParsedExtraConfig;
|
||||
if (typeof extraConfig !== 'string') return {};
|
||||
try {
|
||||
const parsed = JSON.parse(extraConfig) as unknown;
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
||||
if (!isRecord(parsed)) return {};
|
||||
return parsed as ParsedExtraConfig;
|
||||
} catch {
|
||||
return {};
|
||||
@@ -115,7 +123,7 @@ function asModelDiscoveryStatus(value: unknown): OauthModelDiscoveryStatus | und
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseStoredOauthIdentity(extraConfig?: string | null): StoredOauthIdentity | null {
|
||||
function parseStoredOauthIdentity(extraConfig?: ExtraConfigInput): StoredOauthIdentity | null {
|
||||
const parsed = parseExtraConfig(extraConfig).oauth;
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
||||
const accountKey = asTrimmedString(parsed.accountKey) || asTrimmedString(parsed.accountId);
|
||||
@@ -129,7 +137,7 @@ function parseStoredOauthIdentity(extraConfig?: string | null): StoredOauthIdent
|
||||
};
|
||||
}
|
||||
|
||||
function parseStoredOauthRuntimeState(extraConfig?: string | null): Partial<OauthInfo> | null {
|
||||
function parseStoredOauthRuntimeState(extraConfig?: ExtraConfigInput): Partial<OauthInfo> | null {
|
||||
const parsed = parseExtraConfig(extraConfig).oauth;
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
||||
return {
|
||||
@@ -147,7 +155,7 @@ function parseStoredOauthRuntimeState(extraConfig?: string | null): Partial<Oaut
|
||||
};
|
||||
}
|
||||
|
||||
export function getOauthInfoFromExtraConfig(extraConfig?: string | null): OauthInfo | null {
|
||||
export function getOauthInfoFromExtraConfig(extraConfig?: ExtraConfigInput): OauthInfo | null {
|
||||
const identity = parseStoredOauthIdentity(extraConfig);
|
||||
const runtime = parseStoredOauthRuntimeState(extraConfig);
|
||||
const provider = identity?.provider;
|
||||
@@ -212,7 +220,7 @@ export function buildOauthIdentityBackfillPatch(
|
||||
}
|
||||
|
||||
export function buildOauthInfo(
|
||||
extraConfig?: string | null,
|
||||
extraConfig?: ExtraConfigInput,
|
||||
patch: Partial<OauthInfo> = {},
|
||||
): OauthInfo {
|
||||
const provider = patch.provider || getOauthInfoFromExtraConfig(extraConfig)?.provider;
|
||||
|
||||
@@ -21,13 +21,21 @@ import {
|
||||
buildStoredOauthState,
|
||||
buildStoredOauthStateFromAccount,
|
||||
getOauthInfoFromAccount,
|
||||
type OauthInfo,
|
||||
} from './oauthAccount.js';
|
||||
import { buildCodexOauthInfo } from './codexAccount.js';
|
||||
import {
|
||||
buildCodexOauthInfo,
|
||||
type OauthExtraConfigInput,
|
||||
type OauthIdentityCarrierLike,
|
||||
} from './codexAccount.js';
|
||||
import { ensureOauthIdentityBackfill } from './oauthIdentityBackfill.js';
|
||||
import { buildQuotaSnapshotFromOauthInfo, refreshOauthQuotaSnapshot } from './quota.js';
|
||||
|
||||
type OAuthProviderMetadata = ReturnType<typeof listOauthProviders>[number];
|
||||
const MANUAL_CALLBACK_DELAY_MS = 15_000;
|
||||
type OauthProviderHeaderAccountInput = OauthIdentityCarrierLike & {
|
||||
extraConfig?: OauthExtraConfigInput;
|
||||
};
|
||||
|
||||
type OAuthStartInstructions = {
|
||||
redirectUri: string;
|
||||
@@ -597,13 +605,8 @@ export async function startOauthRebindFlow(accountId: number, requestOrigin?: st
|
||||
}
|
||||
|
||||
export function buildOauthProviderHeaders(input: {
|
||||
account?: {
|
||||
extraConfig?: string | null;
|
||||
oauthProvider?: string | null;
|
||||
oauthAccountKey?: string | null;
|
||||
oauthProjectId?: string | null;
|
||||
} | null;
|
||||
extraConfig?: string | null;
|
||||
account?: OauthProviderHeaderAccountInput | null;
|
||||
extraConfig?: OauthExtraConfigInput;
|
||||
downstreamHeaders?: Record<string, unknown>;
|
||||
}) {
|
||||
const oauth = getOauthInfoFromAccount(input.account || {
|
||||
@@ -619,7 +622,7 @@ export function buildOauthProviderHeaders(input: {
|
||||
}
|
||||
|
||||
export function buildCodexOauthProviderHeaders(input: {
|
||||
extraConfig?: string | null;
|
||||
extraConfig?: OauthExtraConfigInput;
|
||||
downstreamHeaders?: Record<string, unknown>;
|
||||
}) {
|
||||
const oauth = buildCodexOauthInfo(input.extraConfig);
|
||||
|
||||
Reference in New Issue
Block a user