refactor: consolidate single-source routing and protocol helpers

This commit is contained in:
Cita
2026-03-22 21:27:09 +08:00
parent 96e7614835
commit 9290e51ecc
64 changed files with 3571 additions and 2681 deletions
@@ -0,0 +1,136 @@
import { describe, expect, it } from 'vitest';
import {
buildOauthInfoFromAccount,
buildStoredOauthStateFromAccount,
getOauthInfoFromAccount,
} from './oauthAccount.js';
describe('oauth account identity helpers', () => {
it('prefers structured oauth identity columns over extraConfig metadata', () => {
const oauth = getOauthInfoFromAccount({
oauthProvider: 'gemini-cli',
oauthAccountKey: 'structured-user@example.com',
oauthProjectId: 'structured-project',
extraConfig: JSON.stringify({
oauth: {
provider: 'codex',
accountKey: 'json-user',
projectId: 'json-project',
refreshToken: 'refresh-token',
quota: { status: 'supported' },
},
}),
});
expect(oauth).toEqual(expect.objectContaining({
provider: 'gemini-cli',
accountId: 'structured-user@example.com',
accountKey: 'structured-user@example.com',
projectId: 'structured-project',
refreshToken: 'refresh-token',
quota: { status: 'supported' },
}));
});
it('falls back to extraConfig metadata when structured columns are absent', () => {
const oauth = getOauthInfoFromAccount({
oauthProvider: null,
oauthAccountKey: null,
oauthProjectId: null,
extraConfig: JSON.stringify({
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',
oauthAccountKey: 'structured-user@example.com',
oauthProjectId: 'structured-project',
extraConfig: JSON.stringify({
oauth: {
email: 'structured-user@example.com',
refreshToken: 'refresh-token',
modelDiscoveryStatus: 'healthy',
},
}),
});
expect(oauth).toEqual(expect.objectContaining({
provider: 'gemini-cli',
accountId: 'structured-user@example.com',
accountKey: 'structured-user@example.com',
projectId: 'structured-project',
email: 'structured-user@example.com',
refreshToken: 'refresh-token',
modelDiscoveryStatus: 'healthy',
}));
});
it('builds patched oauth state from structured identity columns and current runtime state', () => {
const oauth = buildOauthInfoFromAccount({
oauthProvider: 'codex',
oauthAccountKey: 'structured-account',
oauthProjectId: 'structured-project',
extraConfig: JSON.stringify({
oauth: {
provider: 'codex',
refreshToken: 'refresh-token',
modelDiscoveryStatus: 'healthy',
},
}),
}, {
quota: { status: 'supported' } as any,
});
expect(oauth).toEqual(expect.objectContaining({
provider: 'codex',
accountId: 'structured-account',
accountKey: 'structured-account',
projectId: 'structured-project',
refreshToken: 'refresh-token',
modelDiscoveryStatus: 'healthy',
quota: { status: 'supported' },
}));
});
it('strips oauth identity fields when preparing persisted extraConfig state', () => {
const oauth = buildStoredOauthStateFromAccount({
oauthProvider: 'codex',
oauthAccountKey: 'structured-account',
oauthProjectId: 'structured-project',
extraConfig: JSON.stringify({
oauth: {
provider: 'codex',
accountId: 'json-account',
accountKey: 'json-account',
projectId: 'json-project',
refreshToken: 'refresh-token',
modelDiscoveryStatus: 'healthy',
},
}),
}, {
quota: { status: 'supported' } as any,
});
expect(oauth).toEqual({
refreshToken: 'refresh-token',
modelDiscoveryStatus: 'healthy',
quota: { status: 'supported' },
});
});
});
+92 -4
View File
@@ -43,6 +43,15 @@ export type OauthInfo = {
lastDiscoveredModels?: string[];
};
export type StoredOauthState = Omit<OauthInfo, 'provider' | 'accountId' | 'accountKey' | 'projectId'>;
type OauthIdentityCarrier = {
extraConfig?: string | null;
oauthProvider?: string | null;
oauthAccountKey?: string | null;
oauthProjectId?: string | null;
};
function parseExtraConfig(extraConfig?: string | null): ParsedExtraConfig {
if (!extraConfig) return {};
try {
@@ -104,14 +113,12 @@ function asModelDiscoveryStatus(value: unknown): OauthModelDiscoveryStatus | und
return undefined;
}
export function getOauthInfoFromExtraConfig(extraConfig?: string | null): OauthInfo | null {
function parseStoredOauthState(extraConfig?: string | null): Partial<OauthInfo> | null {
const parsed = parseExtraConfig(extraConfig).oauth;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
const provider = asTrimmedString(parsed.provider);
if (!provider) return null;
const accountKey = asTrimmedString(parsed.accountKey) || asTrimmedString(parsed.accountId);
return {
provider,
provider: asTrimmedString(parsed.provider),
accountId: asTrimmedString(parsed.accountId) || accountKey,
accountKey,
email: asTrimmedString(parsed.email),
@@ -129,6 +136,46 @@ export function getOauthInfoFromExtraConfig(extraConfig?: string | null): OauthI
};
}
export function getOauthInfoFromExtraConfig(extraConfig?: string | null): OauthInfo | null {
const parsed = parseStoredOauthState(extraConfig);
const provider = parsed?.provider;
if (!provider) return null;
return {
provider,
accountId: parsed.accountId || parsed.accountKey,
accountKey: parsed.accountKey,
email: parsed.email,
planType: parsed.planType,
projectId: parsed.projectId,
tokenExpiresAt: parsed.tokenExpiresAt,
refreshToken: parsed.refreshToken,
idToken: parsed.idToken,
providerData: parsed.providerData,
quota: parsed.quota,
modelDiscoveryStatus: parsed.modelDiscoveryStatus,
lastModelSyncAt: parsed.lastModelSyncAt,
lastModelSyncError: parsed.lastModelSyncError,
lastDiscoveredModels: parsed.lastDiscoveredModels,
};
}
export function getOauthInfoFromAccount(account?: OauthIdentityCarrier | null): OauthInfo | null {
if (!account) return null;
const stored = parseStoredOauthState(account.extraConfig);
const provider = asTrimmedString(account.oauthProvider) || stored?.provider;
if (!provider) return null;
const structuredAccountKey = asTrimmedString(account.oauthAccountKey);
const accountKey = structuredAccountKey || stored?.accountKey || stored?.accountId;
const projectId = asTrimmedString(account.oauthProjectId) || stored?.projectId;
return {
...(stored || {}),
provider,
accountId: structuredAccountKey || stored?.accountId || accountKey,
accountKey,
projectId,
};
}
export function buildOauthInfo(
extraConfig?: string | null,
patch: Partial<OauthInfo> = {},
@@ -152,6 +199,47 @@ export function buildOauthInfo(
return next;
}
export function buildOauthInfoFromAccount(
account?: OauthIdentityCarrier | null,
patch: Partial<OauthInfo> = {},
): OauthInfo {
const provider = patch.provider || getOauthInfoFromAccount(account)?.provider;
if (!provider) {
throw new Error('oauth provider is required');
}
const current = getOauthInfoFromAccount(account);
const next: OauthInfo = {
provider,
...(current || {}),
...patch,
};
if (!next.accountKey && next.accountId) {
next.accountKey = next.accountId;
}
if (!next.accountId && next.accountKey) {
next.accountId = next.accountKey;
}
return next;
}
export function buildStoredOauthState(oauth: OauthInfo): StoredOauthState {
const {
provider: _provider,
accountId: _accountId,
accountKey: _accountKey,
projectId: _projectId,
...stored
} = oauth;
return stored;
}
export function buildStoredOauthStateFromAccount(
account?: OauthIdentityCarrier | null,
patch: Partial<OauthInfo> = {},
): StoredOauthState {
return buildStoredOauthState(buildOauthInfoFromAccount(account, patch));
}
export function isOauthProvider(
account: Pick<typeof schema.accounts.$inferSelect, 'extraConfig'> | string | null | undefined,
provider?: string,
+9 -5
View File
@@ -1,7 +1,11 @@
import { eq } from 'drizzle-orm';
import { db, schema } from '../../db/index.js';
import { mergeAccountExtraConfig } from '../accountExtraConfig.js';
import { buildOauthInfo, getOauthInfoFromExtraConfig, type OauthInfo } from './oauthAccount.js';
import {
buildStoredOauthStateFromAccount,
getOauthInfoFromAccount,
type OauthInfo,
} from './oauthAccount.js';
import type { OauthQuotaSnapshot, OauthQuotaWindowSnapshot } from './quotaTypes.js';
type CodexJwtClaims = {
@@ -192,12 +196,12 @@ async function persistQuotaSnapshot(accountId: number, snapshot: OauthQuotaSnaps
if (!account) {
throw new Error('oauth account not found');
}
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
const oauth = getOauthInfoFromAccount(account);
if (!oauth) {
throw new Error('account is not managed by oauth');
}
const nextExtraConfig = mergeAccountExtraConfig(account.extraConfig, {
oauth: buildOauthInfo(account.extraConfig, {
oauth: buildStoredOauthStateFromAccount(account, {
quota: snapshot,
}),
});
@@ -213,7 +217,7 @@ export async function refreshOauthQuotaSnapshot(accountId: number): Promise<Oaut
if (!account) {
throw new Error('oauth account not found');
}
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
const oauth = getOauthInfoFromAccount(account);
if (!oauth) {
throw new Error('account is not managed by oauth');
}
@@ -236,7 +240,7 @@ export async function recordOauthQuotaResetHint(input: {
const account = await db.select().from(schema.accounts).where(eq(schema.accounts.id, input.accountId)).get();
if (!account) return null;
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
const oauth = getOauthInfoFromAccount(account);
if (!oauth || oauth.provider !== 'codex') return null;
const baseSnapshot = buildQuotaSnapshotFromOauthInfo({
+24 -10
View File
@@ -14,7 +14,13 @@ import {
listOAuthProviderDefinitions,
type OAuthProviderDefinition,
} from './providers.js';
import { buildOauthInfo, getOauthInfoFromExtraConfig } from './oauthAccount.js';
import {
buildOauthInfo,
buildOauthInfoFromAccount,
buildStoredOauthState,
buildStoredOauthStateFromAccount,
getOauthInfoFromAccount,
} from './oauthAccount.js';
import { buildCodexOauthInfo } from './codexAccount.js';
import { buildQuotaSnapshotFromOauthInfo, refreshOauthQuotaSnapshot } from './quota.js';
@@ -232,7 +238,7 @@ async function upsertOauthAccount(input: {
});
const extraConfig = mergeAccountExtraConfig(existing?.extraConfig, {
credentialMode: 'session',
oauth,
oauth: buildStoredOauthState(oauth),
});
if (existing) {
@@ -511,7 +517,7 @@ export async function listOauthConnections(options: {
}
const items = rows.map((row) => {
const oauth = getOauthInfoFromExtraConfig(row.accounts.extraConfig)!;
const oauth = getOauthInfoFromAccount(row.accounts)!;
const models = modelMap.get(row.accounts.id) || [];
const status = (
oauth.modelDiscoveryStatus === 'abnormal'
@@ -553,8 +559,8 @@ export async function deleteOauthConnection(accountId: number) {
if (!account) {
throw new Error('oauth account not found');
}
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
if (!oauth) {
const normalizedOauth = getOauthInfoFromAccount(account);
if (!normalizedOauth) {
throw new Error('account is not managed by oauth');
}
await db.delete(schema.accounts).where(eq(schema.accounts.id, accountId)).run();
@@ -574,7 +580,7 @@ export async function startOauthRebindFlow(accountId: number, requestOrigin?: st
if (!account) {
throw new Error('oauth account not found');
}
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
const oauth = getOauthInfoFromAccount(account);
if (!oauth) {
throw new Error('account is not managed by oauth');
}
@@ -587,10 +593,18 @@ 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;
downstreamHeaders?: Record<string, unknown>;
}) {
const oauth = getOauthInfoFromExtraConfig(input.extraConfig);
const oauth = getOauthInfoFromAccount(input.account || {
extraConfig: input.extraConfig,
});
if (!oauth) return {};
const definition = getOAuthProviderDefinition(oauth.provider);
if (!definition?.buildProxyHeaders) return {};
@@ -619,7 +633,7 @@ export async function refreshOauthAccessToken(accountId: number) {
if (!account) {
throw new Error('oauth account not found');
}
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
const oauth = getOauthInfoFromAccount(account);
if (!oauth?.refreshToken) {
throw new Error('oauth refresh token missing');
}
@@ -635,7 +649,7 @@ export async function refreshOauthAccessToken(accountId: number) {
providerData: oauth.providerData,
},
});
const nextOauth = buildOauthInfo(account.extraConfig, {
const nextOauth = buildOauthInfoFromAccount(account, {
provider: oauth.provider,
accountId: refreshed.accountId || oauth.accountId,
accountKey: refreshed.accountKey || oauth.accountKey || refreshed.accountId || oauth.accountId,
@@ -652,7 +666,7 @@ export async function refreshOauthAccessToken(accountId: number) {
});
const extraConfig = mergeAccountExtraConfig(account.extraConfig, {
credentialMode: 'session',
oauth: nextOauth,
oauth: buildStoredOauthStateFromAccount(account, nextOauth),
});
await db.update(schema.accounts).set({