Skip to content

Commit 62fd94c

Browse files
committed
fix(login): persist OAuth credentials and normalize cancellation
1 parent aa35c54 commit 62fd94c

1 file changed

Lines changed: 102 additions & 62 deletions

File tree

packages/node-sdk/src/login/flows.ts

Lines changed: 102 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { join } from 'node:path';
2+
13
import {
24
applyKimiOAuthConfig,
35
applyMiniMaxOAuthConfig,
@@ -6,11 +8,13 @@ import {
68
fetchKimiCodingModels,
79
fetchOpenAICodexModels,
810
fetchOpenPlatformModels,
11+
FileTokenStorage,
912
filterModelsByPrefix,
1013
getOpenPlatformById,
1114
miniMaxCodingModels,
1215
minimaxCodingProviderId,
1316
minimaxRegionLabel,
17+
resolveOAuthTokenStorageName,
1418
KIMI_CODING_PROVIDER_ID,
1519
KIMI_OAUTH_PLATFORM_ID,
1620
MINIMAX_OAUTH_PLATFORM_ID_CN,
@@ -29,6 +33,7 @@ import {
2933
type OpenPlatformDefinition,
3034
type ProviderModelInfo,
3135
type PythinkerConfigShape,
36+
type TokenInfo,
3237
} from '@pymodel/pythinker-code-oauth';
3338

3439
import {
@@ -330,38 +335,53 @@ async function handleKimiOAuthLogin(ui: LoginUi): Promise<boolean> {
330335
return false;
331336
}
332337

333-
const picked = await ui.promptModelSelectionForOpenPlatform(models, {
334-
id: KIMI_CODING_PROVIDER_ID,
335-
name: 'Kimi For Coding',
336-
});
337-
if (picked === undefined) return false;
338-
const selectedModel = models.find((model) => model.id === picked.model.id);
339-
if (selectedModel === undefined) return false;
338+
try {
339+
const picked = await ui.promptModelSelectionForOpenPlatform(models, {
340+
id: KIMI_CODING_PROVIDER_ID,
341+
name: 'Kimi For Coding',
342+
});
343+
if (picked === undefined) return false;
344+
const selectedModel = models.find((model) => model.id === picked.model.id);
345+
if (selectedModel === undefined) return false;
340346

341-
controller.signal.throwIfAborted();
342-
const current = await ui.harness.getConfig({ reload: true });
343-
controller.signal.throwIfAborted();
344-
const next = cloneConfig(current);
345-
applyKimiOAuthConfig(next as PythinkerConfigShape, {
346-
accessToken: tokens.accessToken,
347-
refreshToken: tokens.refreshToken,
348-
deviceId: tokens.deviceId,
349-
models,
350-
selectedModel,
351-
thinking: picked.effort !== 'off',
352-
effort: picked.effort === 'off' || picked.effort === 'on' ? undefined : picked.effort,
353-
});
354-
committing = true;
355-
await ui.harness.replaceConfigSections({
356-
providers: next.providers,
357-
models: next.models,
358-
defaultModel: next.defaultModel,
359-
thinking: next.thinking,
360-
});
361-
await ui.refreshConfigAfterLogin();
362-
ui.track('login', { provider: KIMI_CODING_PROVIDER_ID, method: 'oauth' });
363-
ui.showStatus(`Setup complete: Kimi For Coding · ${selectedModel.id}`);
364-
return true;
347+
controller.signal.throwIfAborted();
348+
const current = await ui.harness.getConfig({ reload: true });
349+
controller.signal.throwIfAborted();
350+
const next = cloneConfig(current);
351+
applyKimiOAuthConfig(next as PythinkerConfigShape, {
352+
accessToken: tokens.accessToken,
353+
refreshToken: tokens.refreshToken,
354+
deviceId: tokens.deviceId,
355+
models,
356+
selectedModel,
357+
thinking: picked.effort !== 'off',
358+
effort: picked.effort === 'off' || picked.effort === 'on' ? undefined : picked.effort,
359+
});
360+
await persistOAuthToken(ui, KIMI_CODING_PROVIDER_ID, {
361+
accessToken: tokens.accessToken,
362+
refreshToken: tokens.refreshToken,
363+
expiresAt: Math.floor(tokens.expiresAtMs / 1000),
364+
expiresIn: Math.max(1, Math.floor((tokens.expiresAtMs - Date.now()) / 1000)),
365+
scope: tokens.scope,
366+
tokenType: tokens.tokenType,
367+
metadata: { provider: 'kimi', deviceId: tokens.deviceId },
368+
});
369+
controller.signal.throwIfAborted();
370+
committing = true;
371+
await ui.harness.replaceConfigSections({
372+
providers: next.providers,
373+
models: next.models,
374+
defaultModel: next.defaultModel,
375+
thinking: next.thinking,
376+
});
377+
await ui.refreshConfigAfterLogin();
378+
ui.track('login', { provider: KIMI_CODING_PROVIDER_ID, method: 'oauth' });
379+
ui.showStatus(`Setup complete: Kimi For Coding · ${selectedModel.id}`);
380+
return true;
381+
} catch (error) {
382+
if (controller.signal.aborted) return false;
383+
throw error;
384+
}
365385
} finally {
366386
if (ui.cancelInFlight === cancelLogin) ui.cancelInFlight = undefined;
367387
}
@@ -400,43 +420,63 @@ async function handleMiniMaxOAuthLogin(ui: LoginUi, region: MiniMaxRegion): Prom
400420
}
401421
spinner?.stop({ ok: true, label: 'Authorized.' });
402422

403-
const providerId = minimaxCodingProviderId(region);
404-
const models = [...miniMaxCodingModels()];
405-
const picked = await ui.promptModelSelectionForOpenPlatform(models, {
406-
id: providerId,
407-
name: regionLabel,
408-
});
409-
if (picked === undefined) return false;
410-
const selectedModel = models.find((model) => model.id === picked.model.id);
411-
if (selectedModel === undefined) return false;
423+
try {
424+
const providerId = minimaxCodingProviderId(region);
425+
const models = [...miniMaxCodingModels()];
426+
const picked = await ui.promptModelSelectionForOpenPlatform(models, {
427+
id: providerId,
428+
name: regionLabel,
429+
});
430+
if (picked === undefined) return false;
431+
const selectedModel = models.find((model) => model.id === picked.model.id);
432+
if (selectedModel === undefined) return false;
412433

413-
controller.signal.throwIfAborted();
414-
const current = await ui.harness.getConfig({ reload: true });
415-
controller.signal.throwIfAborted();
416-
const next = cloneConfig(current);
417-
applyMiniMaxOAuthConfig(next as PythinkerConfigShape, region, {
418-
accessToken: tokens.accessToken,
419-
refreshToken: tokens.refreshToken,
420-
selectedModel,
421-
thinking: picked.effort !== 'off',
422-
effort: picked.effort === 'off' || picked.effort === 'on' ? undefined : picked.effort,
423-
});
424-
committing = true;
425-
await ui.harness.replaceConfigSections({
426-
providers: next.providers,
427-
models: next.models,
428-
defaultModel: next.defaultModel,
429-
thinking: next.thinking,
430-
});
431-
await ui.refreshConfigAfterLogin();
432-
ui.track('login', { provider: providerId, method: 'oauth' });
433-
ui.showStatus(`Setup complete: ${regionLabel} · ${selectedModel.id}`);
434-
return true;
434+
controller.signal.throwIfAborted();
435+
const current = await ui.harness.getConfig({ reload: true });
436+
controller.signal.throwIfAborted();
437+
const next = cloneConfig(current);
438+
applyMiniMaxOAuthConfig(next as PythinkerConfigShape, region, {
439+
accessToken: tokens.accessToken,
440+
refreshToken: tokens.refreshToken,
441+
selectedModel,
442+
thinking: picked.effort !== 'off',
443+
effort: picked.effort === 'off' || picked.effort === 'on' ? undefined : picked.effort,
444+
});
445+
await persistOAuthToken(ui, providerId, {
446+
accessToken: tokens.accessToken,
447+
refreshToken: tokens.refreshToken,
448+
expiresAt: Math.floor(tokens.expiresAtMs / 1000),
449+
expiresIn: Math.max(1, Math.floor((tokens.expiresAtMs - Date.now()) / 1000)),
450+
scope: tokens.scope,
451+
tokenType: tokens.tokenType,
452+
metadata: { provider: 'minimax', region },
453+
});
454+
controller.signal.throwIfAborted();
455+
committing = true;
456+
await ui.harness.replaceConfigSections({
457+
providers: next.providers,
458+
models: next.models,
459+
defaultModel: next.defaultModel,
460+
thinking: next.thinking,
461+
});
462+
await ui.refreshConfigAfterLogin();
463+
ui.track('login', { provider: providerId, method: 'oauth' });
464+
ui.showStatus(`Setup complete: ${regionLabel} · ${selectedModel.id}`);
465+
return true;
466+
} catch (error) {
467+
if (controller.signal.aborted) return false;
468+
throw error;
469+
}
435470
} finally {
436471
if (ui.cancelInFlight === cancelLogin) ui.cancelInFlight = undefined;
437472
}
438473
}
439474

475+
async function persistOAuthToken(ui: LoginUi, providerId: string, token: TokenInfo): Promise<void> {
476+
const storage = new FileTokenStorage(join(ui.harness.homeDir, 'credentials'));
477+
await storage.save(resolveOAuthTokenStorageName(`oauth/${providerId}`), token);
478+
}
479+
440480
function cloneConfig(config: PythinkerConfig): PythinkerConfig {
441481
return {
442482
...config,

0 commit comments

Comments
 (0)