mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
feat: add OAuth paste-callback traceability
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { OAUTH_CONFIGS } from '../auth-types';
|
||||
import { diagnoseFailure, formatErrorMessage } from '../oauth-trace/diagnose-failure';
|
||||
import { OAuthTracePhase, type OAuthTraceEvent } from '../oauth-trace/trace-events';
|
||||
|
||||
@@ -195,3 +196,87 @@ describe('formatErrorMessage', () => {
|
||||
expect(lines.some((l) => l.includes('--verbose'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-profile OAuth failure matrix', () => {
|
||||
const providers = Object.keys(OAUTH_CONFIGS);
|
||||
const matrix = [
|
||||
{
|
||||
name: 'url-not-displayed',
|
||||
expectedBranch: 'URL_NOT_DISPLAYED',
|
||||
snapshot: () => [
|
||||
ev(OAuthTracePhase.BinarySpawn),
|
||||
ev(OAuthTracePhase.BinaryExit, { data: { code: 0 } }),
|
||||
],
|
||||
hint: '--verbose',
|
||||
},
|
||||
{
|
||||
name: 'callback-not-observed',
|
||||
expectedBranch: 'CALLBACK_NEVER_OBSERVED',
|
||||
snapshot: () => [
|
||||
ev(OAuthTracePhase.AuthUrlDisplayed),
|
||||
ev(OAuthTracePhase.BrowserOpened),
|
||||
ev(OAuthTracePhase.BinaryExit, { data: { code: 0 } }),
|
||||
],
|
||||
hint: '--no-browser',
|
||||
},
|
||||
{
|
||||
name: 'binary-error',
|
||||
expectedBranch: 'BINARY_ERROR_EXIT',
|
||||
snapshot: () => [
|
||||
ev(OAuthTracePhase.BinarySpawn),
|
||||
ev(OAuthTracePhase.BinaryExit, { data: { code: 2, stderrTail: 'boom' } }),
|
||||
],
|
||||
hint: '--verbose',
|
||||
},
|
||||
{
|
||||
name: 'token-exchange-error',
|
||||
expectedBranch: 'TOKEN_EXCHANGE_REJECTED',
|
||||
snapshot: () => [
|
||||
ev(OAuthTracePhase.PasteCallbackSubmitted),
|
||||
ev(OAuthTracePhase.Error, {
|
||||
error: { code: 'CALLBACK_REJECTED', message: 'invalid_grant' },
|
||||
}),
|
||||
],
|
||||
hint: '--verbose',
|
||||
},
|
||||
{
|
||||
name: 'session-expired',
|
||||
expectedBranch: 'TIMEOUT',
|
||||
snapshot: () => [
|
||||
ev(OAuthTracePhase.PasteCallbackPrompted),
|
||||
ev(OAuthTracePhase.Timeout, { data: { timeoutMs: 600000 } }),
|
||||
],
|
||||
hint: '--auth',
|
||||
},
|
||||
{
|
||||
name: 'token-file-missing',
|
||||
expectedBranch: 'TOKEN_FILE_MISSING_POST_EXIT',
|
||||
snapshot: () => [
|
||||
ev(OAuthTracePhase.AuthUrlDisplayed),
|
||||
ev(OAuthTracePhase.BrowserOpened),
|
||||
ev(OAuthTracePhase.CallbackObservedHeuristic),
|
||||
ev(OAuthTracePhase.BinaryExit, { data: { code: 0 } }),
|
||||
ev(OAuthTracePhase.TokenFileMissing),
|
||||
],
|
||||
hint: 'ccs update',
|
||||
},
|
||||
] as const;
|
||||
|
||||
test.each(providers.flatMap((provider) => matrix.map((scenario) => ({ provider, scenario }))))(
|
||||
'$provider $scenario.name emits branch identifier and remediation hint',
|
||||
({ provider, scenario }) => {
|
||||
reset();
|
||||
const diagnosis = diagnoseFailure(scenario.snapshot());
|
||||
expect(diagnosis.branchId).toBe(scenario.expectedBranch);
|
||||
|
||||
const message = formatErrorMessage(diagnosis, {
|
||||
verbose: false,
|
||||
platform: 'linux',
|
||||
callbackPort: 1455,
|
||||
provider,
|
||||
}).join('\n');
|
||||
expect(message).toContain(scenario.hint);
|
||||
expect(message.includes(`ccs ${provider}`) || message.includes('ccs update')).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import * as path from 'path';
|
||||
import type { ProxyTarget } from '../../proxy/proxy-target-resolver';
|
||||
import { InteractivePrompt } from '../../../utils/prompt';
|
||||
import { getCapturedFetchRequests, mockFetch, restoreFetch } from '../../../../tests/mocks';
|
||||
import { createOAuthTraceRecorder } from '../oauth-trace/trace-recorder';
|
||||
import { OAuthTracePhase } from '../oauth-trace/trace-events';
|
||||
|
||||
const remoteTarget: ProxyTarget = {
|
||||
host: 'proxy.example.com',
|
||||
@@ -291,6 +293,113 @@ describe('resolvePasteCallbackAuthUrl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePasteCallbackMode traceability', () => {
|
||||
it('records paste-callback lifecycle events and redacts callback secrets on exchange rejection', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: /\/v0\/management\/codex-auth-url\?is_webui=true$/,
|
||||
response: {
|
||||
auth_url:
|
||||
'https://auth.example.com/authorize?client_id=public&state=upstream-state-secret',
|
||||
state: 'upstream-state-secret',
|
||||
},
|
||||
},
|
||||
{
|
||||
url: /\/v0\/management\/oauth-callback$/,
|
||||
response: { status: 'error', error: 'invalid_grant: expired code' },
|
||||
status: 400,
|
||||
},
|
||||
]);
|
||||
|
||||
const trace = createOAuthTraceRecorder({
|
||||
sessionId: 'paste-test',
|
||||
provider: 'codex',
|
||||
verbose: false,
|
||||
});
|
||||
const { handlePasteCallbackMode } = await import(
|
||||
`../oauth-handler?paste-trace-rejected=${Date.now()}`
|
||||
);
|
||||
|
||||
const result = await handlePasteCallbackMode(
|
||||
'codex',
|
||||
{ provider: 'codex', displayName: 'Codex', authUrl: '', scopes: [], authFlag: '' },
|
||||
false,
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-paste-trace-rejected-')),
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
trace,
|
||||
promptForCallbackUrl: async () =>
|
||||
'http://localhost:1455/callback?code=oauth-code-secret&state=upstream-state-secret',
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(trace.snapshot().map((event) => event.phase)).toEqual([
|
||||
OAuthTracePhase.AuthUrlDisplayed,
|
||||
OAuthTracePhase.PasteCallbackPrompted,
|
||||
OAuthTracePhase.PasteCallbackReceived,
|
||||
OAuthTracePhase.PasteCallbackSubmitted,
|
||||
OAuthTracePhase.Error,
|
||||
]);
|
||||
expect(JSON.stringify(trace.snapshot())).not.toContain('oauth-code-secret');
|
||||
expect(JSON.stringify(trace.snapshot())).not.toContain('upstream-state-secret');
|
||||
});
|
||||
|
||||
it('records token exchange and token-file-missing when callback succeeds without a local token', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: /\/v0\/management\/codex-auth-url\?is_webui=true$/,
|
||||
response: {
|
||||
auth_url: 'https://auth.example.com/authorize?client_id=public&state=state-123',
|
||||
state: 'state-123',
|
||||
},
|
||||
},
|
||||
{
|
||||
url: /\/v0\/management\/oauth-callback$/,
|
||||
response: { status: 'ok' },
|
||||
},
|
||||
{
|
||||
url: /\/v0\/management\/get-auth-status\?state=state-123$/,
|
||||
response: { status: 'ok' },
|
||||
},
|
||||
]);
|
||||
|
||||
const trace = createOAuthTraceRecorder({
|
||||
sessionId: 'paste-test-missing-token',
|
||||
provider: 'codex',
|
||||
verbose: false,
|
||||
});
|
||||
const { handlePasteCallbackMode } = await import(
|
||||
`../oauth-handler?paste-trace-token-missing=${Date.now()}`
|
||||
);
|
||||
|
||||
const result = await handlePasteCallbackMode(
|
||||
'codex',
|
||||
{ provider: 'codex', displayName: 'Codex', authUrl: '', scopes: [], authFlag: '' },
|
||||
false,
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-paste-trace-token-missing-')),
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
trace,
|
||||
promptForCallbackUrl: async () =>
|
||||
'http://localhost:1455/callback?code=oauth-code-secret&state=state-123',
|
||||
timeoutMs: 5,
|
||||
pollIntervalMs: 1,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(trace.snapshot().map((event) => event.phase)).toContain(
|
||||
OAuthTracePhase.TokenExchangePending
|
||||
);
|
||||
expect(trace.snapshot().map((event) => event.phase)).toContain(
|
||||
OAuthTracePhase.TokenFileMissing
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findNewTokenSnapshotForManualAuth', () => {
|
||||
it('detects newly created provider token files', async () => {
|
||||
const tokenDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-kiro-manual-auth-'));
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fail, info, warn, color, ok } from '../../utils/ui';
|
||||
import { createLogger } from '../../services/logging';
|
||||
import { ensureCLIProxyBinary, getStoredConfiguredBackend } from '../binary-manager';
|
||||
@@ -76,6 +77,11 @@ import {
|
||||
} from '../accounts/account-safety';
|
||||
import { ensureCliAntigravityResponsibility } from '../auth/antigravity-responsibility';
|
||||
import { InteractivePrompt } from '../../utils/prompt';
|
||||
import { getCcsDir } from '../../utils/config-manager';
|
||||
import { generateSessionId } from './project-selection-handler';
|
||||
import { createFileSink } from './oauth-trace/sink-file';
|
||||
import { createOAuthTraceRecorder, OAuthTracePhase, type OAuthTraceRecorder } from './oauth-trace';
|
||||
import { diagnoseFailure, formatErrorMessage } from './oauth-trace/diagnose-failure';
|
||||
|
||||
interface PasteCallbackStartData {
|
||||
url?: string;
|
||||
@@ -84,6 +90,13 @@ interface PasteCallbackStartData {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface PasteCallbackTraceOptions {
|
||||
trace?: OAuthTraceRecorder;
|
||||
promptForCallbackUrl?: (timeoutMs: number) => Promise<string | null>;
|
||||
timeoutMs?: number;
|
||||
pollIntervalMs?: number;
|
||||
}
|
||||
|
||||
const PASTE_CALLBACK_AUTH_URL_POLL_INTERVAL_MS = 3000;
|
||||
const POLLED_AUTH_LOCAL_TOKEN_GRACE_MS = 15 * 1000;
|
||||
const GEMINI_PLUS_CLIENT_ID_ENV = 'CLIPROXY_GEMINI_OAUTH_CLIENT_ID';
|
||||
@@ -641,7 +654,81 @@ export function usesKiroLocalCallbackReplay(
|
||||
* Handle paste-callback mode: show auth URL, prompt for callback paste
|
||||
* Uses proxy target resolver to connect to correct CLIProxyAPI instance (local or remote)
|
||||
*/
|
||||
async function handlePasteCallbackMode(
|
||||
function createPasteCallbackTraceRecorder(
|
||||
provider: CLIProxyProvider,
|
||||
verbose: boolean
|
||||
): OAuthTraceRecorder {
|
||||
const fileSink =
|
||||
process.env['CCS_OAUTH_LOG_FILE'] === '1'
|
||||
? createFileSink({ dir: path.join(getCcsDir(), 'logs') })
|
||||
: undefined;
|
||||
return createOAuthTraceRecorder({
|
||||
sessionId: generateSessionId(),
|
||||
provider,
|
||||
verbose,
|
||||
fileSink,
|
||||
});
|
||||
}
|
||||
|
||||
async function promptForPasteCallbackUrl(timeoutMs: number): Promise<string | null> {
|
||||
const readline = await import('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise<string | null>((resolve) => {
|
||||
let resolved = false;
|
||||
|
||||
rl.on('close', () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(info('After completing authentication, paste the callback URL here:'));
|
||||
rl.question('> ', (answer) => {
|
||||
resolved = true;
|
||||
rl.close();
|
||||
resolve(answer.trim() || null);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
rl.close();
|
||||
console.log('');
|
||||
console.log(
|
||||
fail(`Timed out waiting for callback URL (${Math.round(timeoutMs / 60000)} minutes)`)
|
||||
);
|
||||
resolve(null);
|
||||
}
|
||||
}, timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
function printPasteCallbackTraceDiagnosis(
|
||||
trace: OAuthTraceRecorder,
|
||||
provider: CLIProxyProvider,
|
||||
verbose: boolean
|
||||
): void {
|
||||
const diagnosis = diagnoseFailure(trace.snapshot());
|
||||
if (diagnosis.branchId === 'UNKNOWN') {
|
||||
return;
|
||||
}
|
||||
const callbackPort = OAUTH_PORTS[provider] ?? null;
|
||||
for (const line of formatErrorMessage(diagnosis, {
|
||||
verbose,
|
||||
platform: process.platform,
|
||||
callbackPort,
|
||||
provider,
|
||||
})) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePasteCallbackMode(
|
||||
provider: CLIProxyProvider,
|
||||
oauthConfig: ProviderOAuthConfig,
|
||||
verbose: boolean,
|
||||
@@ -652,12 +739,14 @@ async function handlePasteCallbackMode(
|
||||
kiroMethod?: OAuthOptions['kiroMethod'];
|
||||
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||
add?: boolean;
|
||||
}
|
||||
} & PasteCallbackTraceOptions
|
||||
): Promise<AccountInfo | null> {
|
||||
// Resolve CLIProxyAPI target (local or remote based on config)
|
||||
const target = getProxyTarget();
|
||||
// OAuth state timeout (10 minutes, matches CLIProxyAPI state TTL)
|
||||
const OAUTH_STATE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const OAUTH_STATE_TIMEOUT_MS = options?.timeoutMs ?? 10 * 60 * 1000;
|
||||
const pollIntervalMs = options?.pollIntervalMs ?? PASTE_CALLBACK_AUTH_URL_POLL_INTERVAL_MS;
|
||||
const trace = options?.trace ?? createPasteCallbackTraceRecorder(provider, verbose);
|
||||
|
||||
console.log('');
|
||||
console.log(info(`Starting ${oauthConfig.displayName} OAuth (paste-callback mode)...`));
|
||||
@@ -694,19 +783,37 @@ async function handlePasteCallbackMode(
|
||||
}
|
||||
}
|
||||
warnPossible403Ban(provider, startError);
|
||||
trace.record(OAuthTracePhase.Error, { startPath }, { message: startError });
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
|
||||
const authUrl = await resolvePasteCallbackAuthUrl(target, startData, OAUTH_STATE_TIMEOUT_MS);
|
||||
const authUrl = await resolvePasteCallbackAuthUrl(
|
||||
target,
|
||||
startData,
|
||||
OAUTH_STATE_TIMEOUT_MS,
|
||||
pollIntervalMs
|
||||
);
|
||||
|
||||
if (!authUrl) {
|
||||
console.log(fail('No authorization URL received'));
|
||||
trace.record(OAuthTracePhase.Error, {}, { message: 'No authorization URL received' });
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
|
||||
const authUrlCredentialError = getGeminiAuthUrlCredentialError(provider, authUrl);
|
||||
if (authUrlCredentialError) {
|
||||
console.log(fail(authUrlCredentialError));
|
||||
trace.record(
|
||||
OAuthTracePhase.Error,
|
||||
{},
|
||||
{ code: 'GEMINI_PLUS_MISSING_CRED', message: authUrlCredentialError }
|
||||
);
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -721,47 +828,22 @@ async function handlePasteCallbackMode(
|
||||
console.log('');
|
||||
console.log(` ${authUrl}`);
|
||||
console.log('');
|
||||
trace.record(OAuthTracePhase.AuthUrlDisplayed, { authUrl });
|
||||
|
||||
// Prompt for callback URL
|
||||
const readline = await import('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const callbackUrl = await new Promise<string | null>((resolve) => {
|
||||
let resolved = false;
|
||||
|
||||
rl.on('close', () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(info('After completing authentication, paste the callback URL here:'));
|
||||
rl.question('> ', (answer) => {
|
||||
resolved = true;
|
||||
rl.close();
|
||||
resolve(answer.trim() || null);
|
||||
});
|
||||
|
||||
// Timeout after 10 minutes (match state TTL)
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
rl.close();
|
||||
console.log('');
|
||||
console.log(fail('Timed out waiting for callback URL (10 minutes)'));
|
||||
resolve(null);
|
||||
}
|
||||
}, OAUTH_STATE_TIMEOUT_MS);
|
||||
});
|
||||
trace.record(OAuthTracePhase.PasteCallbackPrompted, { timeoutMs: OAUTH_STATE_TIMEOUT_MS });
|
||||
const callbackUrl = await (options?.promptForCallbackUrl ?? promptForPasteCallbackUrl)(
|
||||
OAUTH_STATE_TIMEOUT_MS
|
||||
);
|
||||
|
||||
if (!callbackUrl) {
|
||||
console.log(info('Cancelled'));
|
||||
trace.record(OAuthTracePhase.Cancelled, { reason: 'empty_callback' });
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
trace.record(OAuthTracePhase.PasteCallbackReceived, { callbackUrl });
|
||||
|
||||
// Validate callback URL
|
||||
let code: string | undefined;
|
||||
@@ -770,11 +852,17 @@ async function handlePasteCallbackMode(
|
||||
code = parsed.searchParams.get('code') || undefined;
|
||||
} catch {
|
||||
console.log(fail('Invalid URL format'));
|
||||
trace.record(OAuthTracePhase.PasteCallbackInvalid, { reason: 'invalid_url' });
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
console.log(fail('Invalid callback URL: missing code parameter'));
|
||||
trace.record(OAuthTracePhase.PasteCallbackInvalid, { reason: 'missing_code' });
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -782,6 +870,7 @@ async function handlePasteCallbackMode(
|
||||
console.log(info('Submitting callback...'));
|
||||
|
||||
const callbackProvider = CLIPROXY_CALLBACK_PROVIDER_MAP[provider] || provider;
|
||||
trace.record(OAuthTracePhase.PasteCallbackSubmitted, { provider: callbackProvider });
|
||||
|
||||
const callbackResponse = await fetch(buildProxyUrl(target, getManagementOAuthCallbackPath()), {
|
||||
method: 'POST',
|
||||
@@ -802,10 +891,18 @@ async function handlePasteCallbackMode(
|
||||
callbackData.error || `OAuth callback failed with status ${callbackResponse.status}`;
|
||||
console.log(fail(callbackError));
|
||||
warnPossible403Ban(provider, callbackError);
|
||||
trace.record(
|
||||
OAuthTracePhase.Error,
|
||||
{ status: callbackResponse.status },
|
||||
{ code: 'CALLBACK_REJECTED', message: callbackError }
|
||||
);
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(info('Callback submitted. Waiting for token exchange...'));
|
||||
trace.record(OAuthTracePhase.TokenExchangePending, { state: oauthState ?? undefined });
|
||||
const { tokenSnapshot, error: tokenWaitError } = await waitForManualCallbackToken(
|
||||
provider,
|
||||
target,
|
||||
@@ -813,12 +910,20 @@ async function handlePasteCallbackMode(
|
||||
oauthState,
|
||||
knownTokenFiles,
|
||||
expectedAccountId,
|
||||
OAUTH_STATE_TIMEOUT_MS
|
||||
OAUTH_STATE_TIMEOUT_MS,
|
||||
pollIntervalMs
|
||||
);
|
||||
|
||||
if (tokenWaitError) {
|
||||
console.log(fail(tokenWaitError));
|
||||
warnPossible403Ban(provider, tokenWaitError);
|
||||
trace.record(
|
||||
OAuthTracePhase.Error,
|
||||
{},
|
||||
{ code: 'CALLBACK_REJECTED', message: tokenWaitError }
|
||||
);
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -828,8 +933,12 @@ async function handlePasteCallbackMode(
|
||||
'Authentication completed upstream, but no new local token was saved for this account. Update CCS/CLIProxy and retry.'
|
||||
)
|
||||
);
|
||||
trace.record(OAuthTracePhase.TokenFileMissing, {});
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
trace.record(OAuthTracePhase.TokenFileAppeared, {});
|
||||
|
||||
const account = registerAccountFromToken(
|
||||
provider,
|
||||
@@ -843,8 +952,12 @@ async function handlePasteCallbackMode(
|
||||
console.log(
|
||||
fail('Authenticated token could not be matched to the requested account. Retry the flow.')
|
||||
);
|
||||
trace.record(OAuthTracePhase.TokenFileMissing, { reason: 'account_match_failed' });
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
await trace.flush();
|
||||
|
||||
console.log(ok('Authentication successful!'));
|
||||
|
||||
@@ -863,6 +976,9 @@ async function handlePasteCallbackMode(
|
||||
} else {
|
||||
console.log(fail('OAuth failed. Use --verbose for details.'));
|
||||
}
|
||||
trace.record(OAuthTracePhase.Error, {}, error as Error);
|
||||
await trace.flush();
|
||||
printPasteCallbackTraceDiagnosis(trace, provider, verbose);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user