fix: improve oauth error and model discovery fallbacks
This commit is contained in:
@@ -385,6 +385,154 @@ describe('oauth routes', { timeout: 15_000 }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('marks gemini oauth session as error when token exchange fails before account persistence', async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: async () => ({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Bad Request',
|
||||
}),
|
||||
text: async () => '{"error":"invalid_grant","error_description":"Bad Request"}',
|
||||
});
|
||||
|
||||
const startResponse = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/oauth/providers/gemini-cli/start',
|
||||
payload: {
|
||||
projectId: 'demo-project',
|
||||
},
|
||||
headers: {
|
||||
host: 'metapi.example',
|
||||
'x-forwarded-proto': 'https',
|
||||
},
|
||||
});
|
||||
const startBody = startResponse.json() as { state: string };
|
||||
|
||||
const callbackResponse = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/oauth/sessions/${encodeURIComponent(startBody.state)}/manual-callback`,
|
||||
payload: {
|
||||
callbackUrl: `http://localhost:8085/oauth2callback?state=${encodeURIComponent(startBody.state)}&code=oauth-code-gemini-123`,
|
||||
},
|
||||
});
|
||||
expect(callbackResponse.statusCode).toBe(500);
|
||||
expect(callbackResponse.json()).toMatchObject({
|
||||
message: expect.stringContaining('invalid_grant'),
|
||||
});
|
||||
|
||||
const sessionResponse = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/oauth/sessions/${startBody.state}`,
|
||||
});
|
||||
expect(sessionResponse.statusCode).toBe(200);
|
||||
expect(sessionResponse.json()).toMatchObject({
|
||||
provider: 'gemini-cli',
|
||||
status: 'error',
|
||||
error: expect.stringContaining('invalid_grant'),
|
||||
});
|
||||
|
||||
const accounts = await db.select().from(schema.accounts).all();
|
||||
expect(accounts).toEqual([]);
|
||||
});
|
||||
|
||||
it('defaults Gemini CLI oauth to the first available Google Cloud project when projectId is omitted', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
access_token: 'gemini-access-token',
|
||||
refresh_token: 'gemini-refresh-token',
|
||||
expires_in: 3600,
|
||||
token_type: 'Bearer',
|
||||
scope: 'cloud-platform',
|
||||
}),
|
||||
text: async () => JSON.stringify({ ok: true }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
projects: [
|
||||
{ projectId: 'first-project-id' },
|
||||
{ projectId: 'second-project-id' },
|
||||
],
|
||||
}),
|
||||
text: async () => JSON.stringify({ ok: true }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ state: 'ENABLED' }),
|
||||
text: async () => JSON.stringify({ state: 'ENABLED' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ email: 'gemini-user@example.com' }),
|
||||
text: async () => JSON.stringify({ email: 'gemini-user@example.com' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ state: 'ENABLED' }),
|
||||
text: async () => JSON.stringify({ state: 'ENABLED' }),
|
||||
});
|
||||
|
||||
const startResponse = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/oauth/providers/gemini-cli/start',
|
||||
headers: {
|
||||
host: 'metapi.example',
|
||||
'x-forwarded-proto': 'https',
|
||||
},
|
||||
});
|
||||
expect(startResponse.statusCode).toBe(200);
|
||||
const startBody = startResponse.json() as { state: string };
|
||||
|
||||
const callbackResponse = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/oauth/sessions/${encodeURIComponent(startBody.state)}/manual-callback`,
|
||||
payload: {
|
||||
callbackUrl: `http://localhost:8085/oauth2callback?state=${encodeURIComponent(startBody.state)}&code=gemini-oauth-code-123`,
|
||||
},
|
||||
});
|
||||
expect(callbackResponse.statusCode).toBe(200);
|
||||
expect(callbackResponse.json()).toEqual({ success: true });
|
||||
|
||||
const sessionResponse = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/oauth/sessions/${startBody.state}`,
|
||||
});
|
||||
expect(sessionResponse.statusCode).toBe(200);
|
||||
expect(sessionResponse.json()).toMatchObject({
|
||||
provider: 'gemini-cli',
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
const accounts = await db.select().from(schema.accounts).all();
|
||||
expect(accounts).toHaveLength(1);
|
||||
expect(accounts[0]).toMatchObject({
|
||||
oauthProvider: 'gemini-cli',
|
||||
oauthProjectId: 'first-project-id',
|
||||
username: 'gemini-user@example.com',
|
||||
accessToken: 'gemini-access-token',
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(accounts[0]?.extraConfig || '{}');
|
||||
expect(parsed.oauth).toMatchObject({
|
||||
provider: 'gemini-cli',
|
||||
email: 'gemini-user@example.com',
|
||||
projectId: 'first-project-id',
|
||||
refreshToken: 'gemini-refresh-token',
|
||||
});
|
||||
|
||||
expect(String(fetchMock.mock.calls[1]?.[0] || '')).toContain('cloudresourcemanager.googleapis.com/v1/projects');
|
||||
expect(String(fetchMock.mock.calls[2]?.[0] || '')).toContain('/projects/first-project-id/services/cloudaicompanion.googleapis.com');
|
||||
expect(String(fetchMock.mock.calls[4]?.[0] || '')).toContain('/projects/first-project-id/services/cloudaicompanion.googleapis.com');
|
||||
});
|
||||
|
||||
it('lists oauth connection health metadata and supports deleting the connection', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'ChatGPT Codex OAuth',
|
||||
|
||||
@@ -482,13 +482,27 @@ describe('refreshModelsForAccount credential discovery', () => {
|
||||
expect(parsed.runtimeHealth?.state).toBe('unhealthy');
|
||||
});
|
||||
|
||||
it('discovers antigravity oauth models via direct credential routing', async () => {
|
||||
it('discovers antigravity oauth models via fetchAvailableModels fallback using the oauth project id', async () => {
|
||||
getApiTokenMock.mockResolvedValue(null);
|
||||
getModelsMock.mockImplementation(async (_baseUrl: string, token: string) => (
|
||||
token === 'antigravity-access-token'
|
||||
? ['gemini-3-pro-preview', 'claude-sonnet-4-5-20250929']
|
||||
: []
|
||||
));
|
||||
getModelsMock.mockRejectedValue(new Error('antigravity oauth discovery should not call adapter.getModels'));
|
||||
undiciFetchMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 503,
|
||||
json: async () => ({ error: 'unavailable' }),
|
||||
text: async () => 'unavailable',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
models: {
|
||||
'gemini-3-pro-preview': { displayName: 'Gemini 3 Pro Preview' },
|
||||
'claude-sonnet-4-5-20250929': { displayName: 'Claude Sonnet 4.5' },
|
||||
},
|
||||
}),
|
||||
text: async () => JSON.stringify({ ok: true }),
|
||||
});
|
||||
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'antigravity-site',
|
||||
@@ -526,11 +540,19 @@ describe('refreshModelsForAccount credential discovery', () => {
|
||||
modelCount: 2,
|
||||
modelsPreview: ['gemini-3-pro-preview', 'claude-sonnet-4-5-20250929'],
|
||||
});
|
||||
expect(getModelsMock).toHaveBeenCalledWith(
|
||||
'https://cloudcode-pa.googleapis.com',
|
||||
'antigravity-access-token',
|
||||
undefined,
|
||||
);
|
||||
expect(getModelsMock).not.toHaveBeenCalled();
|
||||
expect(undiciFetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(String(undiciFetchMock.mock.calls[0]?.[0] || '')).toBe('https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels');
|
||||
expect(String(undiciFetchMock.mock.calls[1]?.[0] || '')).toBe('https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels');
|
||||
expect(undiciFetchMock.mock.calls[0]?.[1]).toMatchObject({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer antigravity-access-token',
|
||||
}),
|
||||
});
|
||||
expect(JSON.parse(String(undiciFetchMock.mock.calls[0]?.[1]?.body || '{}'))).toEqual({
|
||||
project: 'project-demo',
|
||||
});
|
||||
|
||||
const rows = await db.select().from(schema.modelAvailability)
|
||||
.where(eq(schema.modelAvailability.accountId, account.id))
|
||||
|
||||
@@ -22,6 +22,14 @@ import { withSiteRecordProxyRequestInit } from './siteProxy.js';
|
||||
import { getCodexOauthInfoFromExtraConfig, isCodexPlatform } from './oauth/codexAccount.js';
|
||||
import { buildOauthInfo, getOauthInfoFromExtraConfig } from './oauth/oauthAccount.js';
|
||||
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js';
|
||||
import {
|
||||
ANTIGRAVITY_CLIENT_METADATA,
|
||||
ANTIGRAVITY_DAILY_UPSTREAM_BASE_URL,
|
||||
ANTIGRAVITY_GOOGLE_API_CLIENT,
|
||||
ANTIGRAVITY_SANDBOX_DAILY_UPSTREAM_BASE_URL,
|
||||
ANTIGRAVITY_UPSTREAM_BASE_URL,
|
||||
ANTIGRAVITY_USER_AGENT,
|
||||
} from './oauth/antigravityProvider.js';
|
||||
import {
|
||||
GEMINI_CLI_GOOGLE_API_CLIENT,
|
||||
GEMINI_CLI_REQUIRED_SERVICE,
|
||||
@@ -184,6 +192,46 @@ function extractCodexModelIds(payload: unknown): string[] {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string): string {
|
||||
return (baseUrl || '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function extractAntigravityModelIds(payload: unknown): string[] {
|
||||
if (!payload || typeof payload !== 'object') return [];
|
||||
const record = payload as { models?: unknown };
|
||||
if (record.models && typeof record.models === 'object' && !Array.isArray(record.models)) {
|
||||
return Object.keys(record.models).map((name) => name.trim()).filter(Boolean);
|
||||
}
|
||||
if (!Array.isArray(record.models)) return [];
|
||||
return record.models.flatMap((item) => {
|
||||
if (typeof item === 'string') {
|
||||
const trimmed = item.trim();
|
||||
return trimmed ? [trimmed] : [];
|
||||
}
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
||||
const value = item as { id?: unknown; name?: unknown };
|
||||
const id = typeof value.id === 'string'
|
||||
? value.id.trim()
|
||||
: (typeof value.name === 'string' ? value.name.trim() : '');
|
||||
return id ? [id] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function buildAntigravityDiscoveryBaseUrls(siteUrl: string): string[] {
|
||||
const seen = new Set<string>();
|
||||
return [
|
||||
siteUrl,
|
||||
ANTIGRAVITY_UPSTREAM_BASE_URL,
|
||||
ANTIGRAVITY_DAILY_UPSTREAM_BASE_URL,
|
||||
ANTIGRAVITY_SANDBOX_DAILY_UPSTREAM_BASE_URL,
|
||||
].flatMap((candidate) => {
|
||||
const normalized = normalizeBaseUrl(candidate);
|
||||
if (!normalized || seen.has(normalized)) return [];
|
||||
seen.add(normalized);
|
||||
return [normalized];
|
||||
});
|
||||
}
|
||||
|
||||
async function updateOauthModelDiscoveryState(input: {
|
||||
account: typeof schema.accounts.$inferSelect;
|
||||
checkedAt: string;
|
||||
@@ -297,6 +345,51 @@ async function validateGeminiCliOauthConnection(input: {
|
||||
}
|
||||
}
|
||||
|
||||
async function discoverAntigravityModelsFromCloud(input: {
|
||||
site: typeof schema.sites.$inferSelect;
|
||||
account: typeof schema.accounts.$inferSelect;
|
||||
}): Promise<string[]> {
|
||||
const accessToken = (input.account.accessToken || '').trim();
|
||||
if (!accessToken) {
|
||||
throw new Error('antigravity oauth access token missing');
|
||||
}
|
||||
|
||||
const oauth = getOauthInfoFromExtraConfig(input.account.extraConfig);
|
||||
const requestBody = oauth?.projectId ? { project: oauth.projectId } : {};
|
||||
let lastError = '';
|
||||
|
||||
for (const baseUrl of buildAntigravityDiscoveryBaseUrls(input.site.url || ANTIGRAVITY_UPSTREAM_BASE_URL)) {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/v1internal:fetchAvailableModels`,
|
||||
withSiteRecordProxyRequestInit(input.site, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': ANTIGRAVITY_USER_AGENT,
|
||||
'X-Goog-Api-Client': ANTIGRAVITY_GOOGLE_API_CLIENT,
|
||||
'Client-Metadata': ANTIGRAVITY_CLIENT_METADATA,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
}),
|
||||
);
|
||||
if (!response.ok) {
|
||||
lastError = await response.text().catch(() => '') || `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const models = normalizeModels(extractAntigravityModelIds(payload));
|
||||
if (models.length > 0) {
|
||||
return models;
|
||||
}
|
||||
lastError = '未获取到可用模型';
|
||||
}
|
||||
|
||||
throw new Error(lastError || '未获取到可用模型');
|
||||
}
|
||||
|
||||
function isExactModelPattern(modelPattern: string): boolean {
|
||||
const normalized = modelPattern.trim();
|
||||
if (!normalized) return false;
|
||||
@@ -632,6 +725,76 @@ export async function refreshModelsForAccount(accountId: number): Promise<ModelR
|
||||
}
|
||||
}
|
||||
|
||||
if (oauth?.provider === 'antigravity') {
|
||||
const checkedAt = new Date().toISOString();
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const antigravityModels = await withTimeout(
|
||||
() => discoverAntigravityModelsFromCloud({ site, account }),
|
||||
MODEL_DISCOVERY_TIMEOUT_MS,
|
||||
`antigravity model discovery timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`,
|
||||
);
|
||||
if (antigravityModels.length === 0) {
|
||||
throw new Error('未获取到可用模型');
|
||||
}
|
||||
|
||||
await db.insert(schema.modelAvailability).values(
|
||||
antigravityModels.map((modelName) => ({
|
||||
accountId,
|
||||
modelName,
|
||||
available: true,
|
||||
latencyMs: Date.now() - startedAt,
|
||||
checkedAt,
|
||||
})),
|
||||
).run();
|
||||
await updateOauthModelDiscoveryState({
|
||||
account,
|
||||
checkedAt,
|
||||
status: 'healthy',
|
||||
lastDiscoveredModels: antigravityModels,
|
||||
});
|
||||
await setAccountRuntimeHealth(accountId, {
|
||||
state: 'healthy',
|
||||
reason: 'Antigravity OAuth 健康探测成功',
|
||||
source: 'model-discovery',
|
||||
checkedAt,
|
||||
});
|
||||
return buildSuccessfulRefreshResult({
|
||||
accountId,
|
||||
modelCount: antigravityModels.length,
|
||||
modelsPreview: antigravityModels.slice(0, 10),
|
||||
tokenScanned: 0,
|
||||
discoveredByCredential: true,
|
||||
discoveredApiToken: false,
|
||||
});
|
||||
} catch (err) {
|
||||
const rawMessage = (err as { message?: string })?.message || 'antigravity model discovery failed';
|
||||
const errorCode = classifyModelDiscoveryError(rawMessage);
|
||||
const errorMessage = `Antigravity 模型获取失败(${rawMessage})`;
|
||||
await updateOauthModelDiscoveryState({
|
||||
account,
|
||||
checkedAt,
|
||||
status: 'abnormal',
|
||||
lastModelSyncError: errorMessage,
|
||||
lastDiscoveredModels: [],
|
||||
});
|
||||
await setAccountRuntimeHealth(account.id, {
|
||||
state: 'unhealthy',
|
||||
reason: errorMessage,
|
||||
source: 'model-discovery',
|
||||
checkedAt,
|
||||
});
|
||||
return buildFailedRefreshResult({
|
||||
accountId,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
tokenScanned: 0,
|
||||
discoveredByCredential: false,
|
||||
discoveredApiToken: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!adapter) {
|
||||
return buildSkippedRefreshResult(accountId, 'adapter_or_status', '平台不可用或账号未激活');
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export const ANTIGRAVITY_LOOPBACK_CALLBACK_PORT = 51121;
|
||||
export const ANTIGRAVITY_LOOPBACK_CALLBACK_PATH = '/oauth-callback';
|
||||
export const ANTIGRAVITY_LOOPBACK_REDIRECT_URI = `http://localhost:${ANTIGRAVITY_LOOPBACK_CALLBACK_PORT}${ANTIGRAVITY_LOOPBACK_CALLBACK_PATH}`;
|
||||
export const ANTIGRAVITY_UPSTREAM_BASE_URL = 'https://cloudcode-pa.googleapis.com';
|
||||
export const ANTIGRAVITY_DAILY_UPSTREAM_BASE_URL = 'https://daily-cloudcode-pa.googleapis.com';
|
||||
export const ANTIGRAVITY_SANDBOX_DAILY_UPSTREAM_BASE_URL = 'https://daily-cloudcode-pa.sandbox.googleapis.com';
|
||||
export const ANTIGRAVITY_GOOGLE_API_CLIENT = 'google-cloud-sdk vscode_cloudshelleditor/0.1';
|
||||
export const ANTIGRAVITY_USER_AGENT = 'google-api-nodejs-client/9.15.1';
|
||||
export const ANTIGRAVITY_CLIENT_METADATA = '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}';
|
||||
|
||||
@@ -156,16 +156,16 @@ async function resolveGeminiProjectId(accessToken: string, requestedProjectId?:
|
||||
}
|
||||
|
||||
const projects = await fetchGcpProjects(accessToken);
|
||||
if (projects.length === 1 && projects[0]) {
|
||||
await ensureGeminiProjectEnabled(accessToken, projects[0]);
|
||||
return projects[0];
|
||||
}
|
||||
|
||||
if (projects.length <= 0) {
|
||||
throw new Error('no Google Cloud projects available for this account');
|
||||
}
|
||||
|
||||
throw new Error('gemini cli: project selection required');
|
||||
const firstProject = projects[0];
|
||||
if (!firstProject) {
|
||||
throw new Error('no Google Cloud projects available for this account');
|
||||
}
|
||||
await ensureGeminiProjectEnabled(accessToken, firstProject);
|
||||
return firstProject;
|
||||
}
|
||||
|
||||
export const geminiCliOauthProvider: OAuthProviderDefinition = {
|
||||
|
||||
@@ -356,40 +356,50 @@ export async function handleOauthCallback(input: {
|
||||
throw new Error('missing oauth code');
|
||||
}
|
||||
|
||||
const exchange = await definition.exchangeAuthorizationCode({
|
||||
code,
|
||||
state: input.state,
|
||||
redirectUri: session.redirectUri,
|
||||
codeVerifier: session.codeVerifier,
|
||||
projectId: session.projectId,
|
||||
});
|
||||
const { account, site, created } = await upsertOauthAccount({
|
||||
definition,
|
||||
exchange,
|
||||
rebindAccountId: session.rebindAccountId,
|
||||
});
|
||||
if (!account) {
|
||||
markOauthSessionError(input.state, 'failed to persist oauth account');
|
||||
throw new Error('failed to persist oauth account');
|
||||
}
|
||||
|
||||
const refreshResult = await refreshModelsForAccount(account.id);
|
||||
if (refreshResult.status !== 'success') {
|
||||
if (created) {
|
||||
await db.delete(schema.accounts).where(eq(schema.accounts.id, account.id)).run();
|
||||
try {
|
||||
const exchange = await definition.exchangeAuthorizationCode({
|
||||
code,
|
||||
state: input.state,
|
||||
redirectUri: session.redirectUri,
|
||||
codeVerifier: session.codeVerifier,
|
||||
projectId: session.projectId,
|
||||
});
|
||||
const { account, site, created } = await upsertOauthAccount({
|
||||
definition,
|
||||
exchange,
|
||||
rebindAccountId: session.rebindAccountId,
|
||||
});
|
||||
if (!account) {
|
||||
markOauthSessionError(input.state, 'failed to persist oauth account');
|
||||
throw new Error('failed to persist oauth account');
|
||||
}
|
||||
await rebuildTokenRoutesFromAvailability();
|
||||
const errorMessage = refreshResult.errorMessage || `${input.provider} model discovery failed`;
|
||||
markOauthSessionError(input.state, errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
await rebuildTokenRoutesFromAvailability();
|
||||
markOauthSessionSuccess(input.state, {
|
||||
accountId: account.id,
|
||||
siteId: site.id,
|
||||
});
|
||||
return { accountId: account.id, siteId: site.id };
|
||||
const refreshResult = await refreshModelsForAccount(account.id);
|
||||
if (refreshResult.status !== 'success') {
|
||||
if (created) {
|
||||
await db.delete(schema.accounts).where(eq(schema.accounts.id, account.id)).run();
|
||||
}
|
||||
await rebuildTokenRoutesFromAvailability();
|
||||
const errorMessage = refreshResult.errorMessage || `${input.provider} model discovery failed`;
|
||||
markOauthSessionError(input.state, errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
await rebuildTokenRoutesFromAvailability();
|
||||
markOauthSessionSuccess(input.state, {
|
||||
accountId: account.id,
|
||||
siteId: site.id,
|
||||
});
|
||||
return { accountId: account.id, siteId: site.id };
|
||||
} catch (error) {
|
||||
const message = (
|
||||
error instanceof Error
|
||||
? (error.message || error.name)
|
||||
: String(error || 'OAuth failed')
|
||||
).trim() || 'OAuth failed';
|
||||
markOauthSessionError(input.state, message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitOauthManualCallback(input: {
|
||||
|
||||
Reference in New Issue
Block a user