byterover-cli 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/infra/llm/providers/openai.d.ts +12 -0
- package/dist/agent/infra/llm/providers/openai.js +52 -1
- package/dist/oclif/commands/curate/index.js +2 -2
- package/dist/oclif/commands/model/switch.js +14 -3
- package/dist/oclif/commands/providers/connect.d.ts +9 -0
- package/dist/oclif/commands/providers/connect.js +110 -14
- package/dist/oclif/commands/providers/list.js +3 -5
- package/dist/oclif/commands/query.js +2 -2
- package/dist/oclif/lib/daemon-client.d.ts +4 -0
- package/dist/oclif/lib/daemon-client.js +13 -3
- package/dist/server/core/domain/entities/provider-config.d.ts +6 -0
- package/dist/server/core/domain/entities/provider-config.js +4 -3
- package/dist/server/core/domain/entities/provider-registry.d.ts +41 -1
- package/dist/server/core/domain/entities/provider-registry.js +24 -1
- package/dist/server/core/domain/errors/task-error.d.ts +2 -0
- package/dist/server/core/domain/errors/task-error.js +6 -1
- package/dist/server/core/domain/transport/schemas.d.ts +2 -0
- package/dist/server/core/interfaces/i-provider-config-store.d.ts +2 -0
- package/dist/server/core/interfaces/i-provider-model-fetcher.d.ts +12 -3
- package/dist/server/core/interfaces/i-provider-oauth-token-store.d.ts +24 -0
- package/dist/server/core/interfaces/i-provider-oauth-token-store.js +1 -0
- package/dist/server/core/interfaces/i-token-refresh-manager.d.ts +7 -0
- package/dist/server/core/interfaces/i-token-refresh-manager.js +1 -0
- package/dist/server/infra/daemon/agent-process.js +22 -4
- package/dist/server/infra/daemon/brv-server.js +13 -2
- package/dist/server/infra/http/models-dev-client.d.ts +29 -0
- package/dist/server/infra/http/models-dev-client.js +133 -0
- package/dist/server/infra/http/provider-model-fetcher-registry.d.ts +2 -1
- package/dist/server/infra/http/provider-model-fetcher-registry.js +6 -1
- package/dist/server/infra/http/provider-model-fetchers.d.ts +33 -8
- package/dist/server/infra/http/provider-model-fetchers.js +88 -10
- package/dist/server/infra/process/feature-handlers.d.ts +3 -1
- package/dist/server/infra/process/feature-handlers.js +3 -1
- package/dist/server/infra/provider/provider-config-resolver.d.ts +4 -2
- package/dist/server/infra/provider/provider-config-resolver.js +59 -4
- package/dist/server/infra/provider-oauth/callback-server.d.ts +24 -0
- package/dist/server/infra/provider-oauth/callback-server.js +203 -0
- package/dist/server/infra/provider-oauth/errors.d.ts +39 -0
- package/dist/server/infra/provider-oauth/errors.js +76 -0
- package/dist/server/infra/provider-oauth/index.d.ts +9 -0
- package/dist/server/infra/provider-oauth/index.js +9 -0
- package/dist/server/infra/provider-oauth/jwt-utils.d.ts +17 -0
- package/dist/server/infra/provider-oauth/jwt-utils.js +51 -0
- package/dist/server/infra/provider-oauth/pkce-service.d.ts +22 -0
- package/dist/server/infra/provider-oauth/pkce-service.js +33 -0
- package/dist/server/infra/provider-oauth/provider-oauth-token-store.d.ts +48 -0
- package/dist/server/infra/provider-oauth/provider-oauth-token-store.js +155 -0
- package/dist/server/infra/provider-oauth/refresh-token-exchange.d.ts +8 -0
- package/dist/server/infra/provider-oauth/refresh-token-exchange.js +39 -0
- package/dist/server/infra/provider-oauth/token-exchange.d.ts +8 -0
- package/dist/server/infra/provider-oauth/token-exchange.js +44 -0
- package/dist/server/infra/provider-oauth/token-refresh-manager.d.ts +32 -0
- package/dist/server/infra/provider-oauth/token-refresh-manager.js +96 -0
- package/dist/server/infra/provider-oauth/types.d.ts +55 -0
- package/dist/server/infra/provider-oauth/types.js +22 -0
- package/dist/server/infra/storage/file-provider-config-store.d.ts +2 -0
- package/dist/server/infra/storage/file-provider-config-store.js +1 -3
- package/dist/server/infra/transport/handlers/model-handler.d.ts +3 -0
- package/dist/server/infra/transport/handlers/model-handler.js +53 -11
- package/dist/server/infra/transport/handlers/provider-handler.d.ts +26 -0
- package/dist/server/infra/transport/handlers/provider-handler.js +215 -13
- package/dist/shared/constants/oauth.d.ts +14 -0
- package/dist/shared/constants/oauth.js +14 -0
- package/dist/shared/transport/events/index.d.ts +5 -0
- package/dist/shared/transport/events/model-events.d.ts +2 -0
- package/dist/shared/transport/events/provider-events.d.ts +36 -0
- package/dist/shared/transport/events/provider-events.js +5 -0
- package/dist/shared/transport/types/dto.d.ts +4 -0
- package/dist/tui/features/model/api/set-active-model.d.ts +1 -1
- package/dist/tui/features/model/api/set-active-model.js +12 -4
- package/dist/tui/features/provider/api/await-oauth-callback.d.ts +11 -0
- package/dist/tui/features/provider/api/await-oauth-callback.js +25 -0
- package/dist/tui/features/provider/api/cancel-oauth.d.ts +5 -0
- package/dist/tui/features/provider/api/cancel-oauth.js +10 -0
- package/dist/tui/features/provider/api/start-oauth.d.ts +11 -0
- package/dist/tui/features/provider/api/start-oauth.js +15 -0
- package/dist/tui/features/provider/components/auth-method-dialog.d.ts +9 -0
- package/dist/tui/features/provider/components/auth-method-dialog.js +20 -0
- package/dist/tui/features/provider/components/oauth-dialog.d.ts +9 -0
- package/dist/tui/features/provider/components/oauth-dialog.js +96 -0
- package/dist/tui/features/provider/components/provider-dialog.js +1 -1
- package/dist/tui/features/provider/components/provider-flow.js +54 -4
- package/dist/tui/features/provider/components/provider-subscription-initializer.d.ts +1 -0
- package/dist/tui/features/provider/components/provider-subscription-initializer.js +5 -0
- package/dist/tui/features/provider/hooks/use-provider-subscriptions.d.ts +6 -0
- package/dist/tui/features/provider/hooks/use-provider-subscriptions.js +24 -0
- package/dist/tui/providers/app-providers.js +2 -1
- package/dist/tui/utils/error-messages.js +6 -1
- package/oclif.manifest.json +132 -116
- package/package.json +1 -1
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { ModelEvents, } from '../../../../shared/transport/events/model-events.js';
|
|
2
2
|
import { TransportDaemonEventNames } from '../../../core/domain/transport/schemas.js';
|
|
3
|
-
import { getModelFetcher } from '../../http/provider-model-fetcher-registry.js';
|
|
3
|
+
import { getModelFetcher as getModelFetcherDefault } from '../../http/provider-model-fetcher-registry.js';
|
|
4
4
|
/**
|
|
5
5
|
* Handles model:* events.
|
|
6
6
|
* Business logic for model listing and selection — no terminal/UI calls.
|
|
7
7
|
*/
|
|
8
8
|
export class ModelHandler {
|
|
9
|
+
getModelFetcher;
|
|
9
10
|
providerConfigStore;
|
|
10
11
|
providerKeychainStore;
|
|
11
12
|
transport;
|
|
12
13
|
constructor(deps) {
|
|
14
|
+
this.getModelFetcher = deps.getModelFetcher ?? getModelFetcherDefault;
|
|
13
15
|
this.providerConfigStore = deps.providerConfigStore;
|
|
14
16
|
this.providerKeychainStore = deps.providerKeychainStore;
|
|
15
17
|
this.transport = deps.transport;
|
|
@@ -22,18 +24,21 @@ export class ModelHandler {
|
|
|
22
24
|
setupList() {
|
|
23
25
|
this.transport.onRequest(ModelEvents.LIST, async (data) => {
|
|
24
26
|
const { providerId } = data;
|
|
25
|
-
const fetcher = await getModelFetcher(providerId);
|
|
27
|
+
const fetcher = await this.getModelFetcher(providerId);
|
|
26
28
|
if (!fetcher) {
|
|
27
29
|
return { favorites: [], models: [], recent: [] };
|
|
28
30
|
}
|
|
29
31
|
// Fetch models from provider API using the correct per-provider fetcher
|
|
30
32
|
let fetchedModels;
|
|
31
33
|
try {
|
|
34
|
+
const config = await this.providerConfigStore.read();
|
|
35
|
+
const authMethod = config.providers[providerId]?.authMethod;
|
|
32
36
|
const apiKey = await this.providerKeychainStore.getApiKey(providerId);
|
|
33
|
-
fetchedModels = await fetcher.fetchModels(apiKey ?? '');
|
|
37
|
+
fetchedModels = await fetcher.fetchModels(apiKey ?? '', { authMethod });
|
|
34
38
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : 'Failed to load models';
|
|
41
|
+
return { error: message, favorites: [], models: [], recent: [] };
|
|
37
42
|
}
|
|
38
43
|
const models = fetchedModels.map((m) => ({
|
|
39
44
|
contextLength: m.contextLength,
|
|
@@ -61,12 +66,14 @@ export class ModelHandler {
|
|
|
61
66
|
setupListByProviders() {
|
|
62
67
|
this.transport.onRequest(ModelEvents.LIST_BY_PROVIDERS, async (data) => {
|
|
63
68
|
const { providerIds } = data;
|
|
69
|
+
const config = await this.providerConfigStore.read();
|
|
64
70
|
const results = await Promise.allSettled(providerIds.map(async (providerId) => {
|
|
65
|
-
const fetcher = await getModelFetcher(providerId);
|
|
71
|
+
const fetcher = await this.getModelFetcher(providerId);
|
|
66
72
|
if (!fetcher)
|
|
67
73
|
return [];
|
|
74
|
+
const authMethod = config.providers[providerId]?.authMethod;
|
|
68
75
|
const apiKey = await this.providerKeychainStore.getApiKey(providerId);
|
|
69
|
-
const fetchedModels = await fetcher.fetchModels(apiKey ?? '');
|
|
76
|
+
const fetchedModels = await fetcher.fetchModels(apiKey ?? '', { authMethod });
|
|
70
77
|
return fetchedModels.map((model) => ({
|
|
71
78
|
contextLength: model.contextLength,
|
|
72
79
|
description: model.description,
|
|
@@ -98,10 +105,45 @@ export class ModelHandler {
|
|
|
98
105
|
}
|
|
99
106
|
setupSetActive() {
|
|
100
107
|
this.transport.onRequest(ModelEvents.SET_ACTIVE, async (data) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
try {
|
|
109
|
+
const config = await this.providerConfigStore.read();
|
|
110
|
+
const providerConfig = config.providers[data.providerId];
|
|
111
|
+
if (!providerConfig) {
|
|
112
|
+
return {
|
|
113
|
+
error: `Provider "${data.providerId}" is not connected`,
|
|
114
|
+
success: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
let matchedModel;
|
|
118
|
+
// Validate model against allowed list for OAuth providers
|
|
119
|
+
if (providerConfig.authMethod === 'oauth') {
|
|
120
|
+
const fetcher = await this.getModelFetcher(data.providerId);
|
|
121
|
+
if (!fetcher) {
|
|
122
|
+
return {
|
|
123
|
+
error: `Cannot validate model for OAuth-connected ${data.providerId}: model fetcher unavailable`,
|
|
124
|
+
success: false,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const allowedModels = await fetcher.fetchModels('', { authMethod: 'oauth' });
|
|
128
|
+
matchedModel = allowedModels.find((m) => m.id === data.modelId);
|
|
129
|
+
if (!matchedModel) {
|
|
130
|
+
const allowedIds = allowedModels.map((m) => m.id).join(', ');
|
|
131
|
+
return {
|
|
132
|
+
error: `Model "${data.modelId}" is not available for OAuth-connected ${data.providerId}. Allowed models: ${allowedIds}`,
|
|
133
|
+
success: false,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const contextLength = data.contextLength ?? matchedModel?.contextLength;
|
|
138
|
+
await this.providerConfigStore.setActiveProvider(data.providerId);
|
|
139
|
+
await this.providerConfigStore.setActiveModel(data.providerId, data.modelId, contextLength);
|
|
140
|
+
this.transport.broadcast(TransportDaemonEventNames.PROVIDER_UPDATED, {});
|
|
141
|
+
return { success: true };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : 'Failed to set active model';
|
|
145
|
+
return { error: message, success: false };
|
|
146
|
+
}
|
|
105
147
|
});
|
|
106
148
|
}
|
|
107
149
|
}
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import type { IProviderConfigStore } from '../../../core/interfaces/i-provider-config-store.js';
|
|
2
2
|
import type { IProviderKeychainStore } from '../../../core/interfaces/i-provider-keychain-store.js';
|
|
3
|
+
import type { IProviderOAuthTokenStore } from '../../../core/interfaces/i-provider-oauth-token-store.js';
|
|
4
|
+
import type { IBrowserLauncher } from '../../../core/interfaces/services/i-browser-launcher.js';
|
|
3
5
|
import type { ITransportServer } from '../../../core/interfaces/transport/i-transport-server.js';
|
|
6
|
+
import type { PkceParameters, ProviderTokenResponse, TokenExchangeParams } from '../../provider-oauth/index.js';
|
|
7
|
+
import { ProviderCallbackServer } from '../../provider-oauth/index.js';
|
|
4
8
|
export interface ProviderHandlerDeps {
|
|
9
|
+
browserLauncher: IBrowserLauncher;
|
|
10
|
+
/** Factory for creating callback servers (injectable for testing) */
|
|
11
|
+
createCallbackServer?: (options: {
|
|
12
|
+
callbackPath?: string;
|
|
13
|
+
port: number;
|
|
14
|
+
}) => ProviderCallbackServer;
|
|
15
|
+
/** Token exchange function (injectable for testing) */
|
|
16
|
+
exchangeCodeForTokens?: (params: TokenExchangeParams) => Promise<ProviderTokenResponse>;
|
|
17
|
+
/** PKCE generator function (injectable for testing) */
|
|
18
|
+
generatePkce?: () => PkceParameters;
|
|
5
19
|
providerConfigStore: IProviderConfigStore;
|
|
6
20
|
providerKeychainStore: IProviderKeychainStore;
|
|
21
|
+
providerOAuthTokenStore: IProviderOAuthTokenStore;
|
|
7
22
|
transport: ITransportServer;
|
|
8
23
|
}
|
|
9
24
|
/**
|
|
@@ -11,15 +26,26 @@ export interface ProviderHandlerDeps {
|
|
|
11
26
|
* Business logic for provider management — no terminal/UI calls.
|
|
12
27
|
*/
|
|
13
28
|
export declare class ProviderHandler {
|
|
29
|
+
private readonly browserLauncher;
|
|
30
|
+
private readonly createCallbackServer;
|
|
31
|
+
private readonly exchangeCodeForTokens;
|
|
32
|
+
private readonly generatePkce;
|
|
33
|
+
private readonly oauthFlows;
|
|
14
34
|
private readonly providerConfigStore;
|
|
15
35
|
private readonly providerKeychainStore;
|
|
36
|
+
private readonly providerOAuthTokenStore;
|
|
16
37
|
private readonly transport;
|
|
17
38
|
constructor(deps: ProviderHandlerDeps);
|
|
18
39
|
setup(): void;
|
|
40
|
+
private cleanupFlowsForClient;
|
|
41
|
+
private setupAwaitOAuthCallback;
|
|
42
|
+
private setupCancelOAuth;
|
|
19
43
|
private setupConnect;
|
|
20
44
|
private setupDisconnect;
|
|
21
45
|
private setupGetActive;
|
|
22
46
|
private setupList;
|
|
23
47
|
private setupSetActive;
|
|
48
|
+
private setupStartOAuth;
|
|
49
|
+
private setupSubmitOAuthCode;
|
|
24
50
|
private setupValidateApiKey;
|
|
25
51
|
}
|
|
@@ -4,17 +4,29 @@ import { TransportDaemonEventNames } from '../../../core/domain/transport/schema
|
|
|
4
4
|
import { getErrorMessage } from '../../../utils/error-helpers.js';
|
|
5
5
|
import { processLog } from '../../../utils/process-logger.js';
|
|
6
6
|
import { validateApiKey as validateApiKeyViaFetcher } from '../../http/provider-model-fetcher-registry.js';
|
|
7
|
+
import { computeExpiresAt, exchangeCodeForTokens as defaultExchangeCodeForTokens, generatePkce as defaultGeneratePkce, parseAccountIdFromIdToken, ProviderCallbackServer, ProviderCallbackTimeoutError, } from '../../provider-oauth/index.js';
|
|
7
8
|
/**
|
|
8
9
|
* Handles provider:* events.
|
|
9
10
|
* Business logic for provider management — no terminal/UI calls.
|
|
10
11
|
*/
|
|
11
12
|
export class ProviderHandler {
|
|
13
|
+
browserLauncher;
|
|
14
|
+
createCallbackServer;
|
|
15
|
+
exchangeCodeForTokens;
|
|
16
|
+
generatePkce;
|
|
17
|
+
oauthFlows = new Map();
|
|
12
18
|
providerConfigStore;
|
|
13
19
|
providerKeychainStore;
|
|
20
|
+
providerOAuthTokenStore;
|
|
14
21
|
transport;
|
|
15
22
|
constructor(deps) {
|
|
23
|
+
this.browserLauncher = deps.browserLauncher;
|
|
24
|
+
this.createCallbackServer = deps.createCallbackServer ?? ((options) => new ProviderCallbackServer(options));
|
|
25
|
+
this.exchangeCodeForTokens = deps.exchangeCodeForTokens ?? defaultExchangeCodeForTokens;
|
|
26
|
+
this.generatePkce = deps.generatePkce ?? defaultGeneratePkce;
|
|
16
27
|
this.providerConfigStore = deps.providerConfigStore;
|
|
17
28
|
this.providerKeychainStore = deps.providerKeychainStore;
|
|
29
|
+
this.providerOAuthTokenStore = deps.providerOAuthTokenStore;
|
|
18
30
|
this.transport = deps.transport;
|
|
19
31
|
}
|
|
20
32
|
setup() {
|
|
@@ -24,6 +36,99 @@ export class ProviderHandler {
|
|
|
24
36
|
this.setupList();
|
|
25
37
|
this.setupSetActive();
|
|
26
38
|
this.setupValidateApiKey();
|
|
39
|
+
this.setupStartOAuth();
|
|
40
|
+
this.setupAwaitOAuthCallback();
|
|
41
|
+
this.setupCancelOAuth();
|
|
42
|
+
this.setupSubmitOAuthCode();
|
|
43
|
+
// Clean up OAuth flows when a client disconnects (prevents callback server port leaks)
|
|
44
|
+
this.transport.onDisconnection((clientId) => {
|
|
45
|
+
this.cleanupFlowsForClient(clientId);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
cleanupFlowsForClient(clientId) {
|
|
49
|
+
for (const [providerId, flow] of this.oauthFlows.entries()) {
|
|
50
|
+
if (flow.clientId === clientId) {
|
|
51
|
+
flow.callbackServer?.stop().catch(() => { });
|
|
52
|
+
this.oauthFlows.delete(providerId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
setupAwaitOAuthCallback() {
|
|
57
|
+
this.transport.onRequest(ProviderEvents.AWAIT_OAUTH_CALLBACK, async (data) => {
|
|
58
|
+
const flow = this.oauthFlows.get(data.providerId);
|
|
59
|
+
if (!flow?.callbackServer) {
|
|
60
|
+
return { error: 'No active OAuth flow for this provider', success: false };
|
|
61
|
+
}
|
|
62
|
+
if (flow.awaitInProgress) {
|
|
63
|
+
return { error: 'OAuth callback is already being awaited for this provider', success: false };
|
|
64
|
+
}
|
|
65
|
+
flow.awaitInProgress = true;
|
|
66
|
+
try {
|
|
67
|
+
// Block until callback or timeout (5 min default in ProviderCallbackServer)
|
|
68
|
+
const callbackResult = await flow.callbackServer.waitForCallback(flow.state);
|
|
69
|
+
// Exchange code for tokens
|
|
70
|
+
const providerDef = getProviderById(data.providerId);
|
|
71
|
+
if (!providerDef?.oauth) {
|
|
72
|
+
return { error: 'Provider does not support OAuth', success: false };
|
|
73
|
+
}
|
|
74
|
+
const oauthConfig = providerDef.oauth;
|
|
75
|
+
const contentType = oauthConfig.tokenContentType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json';
|
|
76
|
+
const tokens = await this.exchangeCodeForTokens({
|
|
77
|
+
clientId: oauthConfig.clientId,
|
|
78
|
+
code: callbackResult.code,
|
|
79
|
+
codeVerifier: flow.codeVerifier,
|
|
80
|
+
contentType,
|
|
81
|
+
redirectUri: oauthConfig.redirectUri,
|
|
82
|
+
tokenUrl: oauthConfig.tokenUrl,
|
|
83
|
+
});
|
|
84
|
+
// Parse JWT id_token for account ID
|
|
85
|
+
const oauthAccountId = tokens.id_token ? parseAccountIdFromIdToken(tokens.id_token) : undefined;
|
|
86
|
+
// Store access token as the "API key" in keychain
|
|
87
|
+
await this.providerKeychainStore.setApiKey(data.providerId, tokens.access_token);
|
|
88
|
+
// Store refresh token + expiry in encrypted OAuth token store
|
|
89
|
+
if (tokens.refresh_token) {
|
|
90
|
+
const expiresAt = tokens.expires_in ? computeExpiresAt(tokens.expires_in) : computeExpiresAt(3600); // 1-hour default when provider omits expires_in
|
|
91
|
+
await this.providerOAuthTokenStore.set(data.providerId, {
|
|
92
|
+
expiresAt,
|
|
93
|
+
refreshToken: tokens.refresh_token,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Connect provider — secrets stored in keychain + encrypted token store, not config
|
|
97
|
+
// OAuth providers may define their own default model (e.g., Codex for OpenAI OAuth)
|
|
98
|
+
const defaultModel = oauthConfig.defaultModel ?? providerDef.defaultModel;
|
|
99
|
+
await this.providerConfigStore.connectProvider(data.providerId, {
|
|
100
|
+
activeModel: defaultModel,
|
|
101
|
+
authMethod: 'oauth',
|
|
102
|
+
oauthAccountId,
|
|
103
|
+
});
|
|
104
|
+
// Broadcast update
|
|
105
|
+
this.transport.broadcast(TransportDaemonEventNames.PROVIDER_UPDATED, {});
|
|
106
|
+
return { success: true };
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof ProviderCallbackTimeoutError) {
|
|
110
|
+
return { error: 'Authentication timed out. Please try again.', success: false };
|
|
111
|
+
}
|
|
112
|
+
return { error: getErrorMessage(error), success: false };
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
// Only clean up if this is still the same flow (guard against concurrent START_OAUTH)
|
|
116
|
+
if (this.oauthFlows.get(data.providerId) === flow) {
|
|
117
|
+
await flow.callbackServer?.stop().catch(() => { });
|
|
118
|
+
this.oauthFlows.delete(data.providerId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
setupCancelOAuth() {
|
|
124
|
+
this.transport.onRequest(ProviderEvents.CANCEL_OAUTH, async (data) => {
|
|
125
|
+
const flow = this.oauthFlows.get(data.providerId);
|
|
126
|
+
if (flow?.callbackServer) {
|
|
127
|
+
await flow.callbackServer.stop().catch(() => { });
|
|
128
|
+
}
|
|
129
|
+
this.oauthFlows.delete(data.providerId);
|
|
130
|
+
return { success: true };
|
|
131
|
+
});
|
|
27
132
|
}
|
|
28
133
|
setupConnect() {
|
|
29
134
|
this.transport.onRequest(ProviderEvents.CONNECT, async (data) => {
|
|
@@ -35,6 +140,7 @@ export class ProviderHandler {
|
|
|
35
140
|
const provider = getProviderById(providerId);
|
|
36
141
|
await this.providerConfigStore.connectProvider(providerId, {
|
|
37
142
|
activeModel: provider?.defaultModel,
|
|
143
|
+
authMethod: apiKey ? 'api-key' : undefined,
|
|
38
144
|
baseUrl,
|
|
39
145
|
});
|
|
40
146
|
this.transport.broadcast(TransportDaemonEventNames.PROVIDER_UPDATED, {});
|
|
@@ -46,6 +152,7 @@ export class ProviderHandler {
|
|
|
46
152
|
const { providerId } = data;
|
|
47
153
|
await this.providerConfigStore.disconnectProvider(providerId);
|
|
48
154
|
await this.providerKeychainStore.deleteApiKey(providerId);
|
|
155
|
+
await this.providerOAuthTokenStore.delete(providerId);
|
|
49
156
|
this.transport.broadcast(TransportDaemonEventNames.PROVIDER_UPDATED, {});
|
|
50
157
|
return { success: true };
|
|
51
158
|
});
|
|
@@ -64,19 +171,28 @@ export class ProviderHandler {
|
|
|
64
171
|
processLog(`[ProviderHandler] getActiveProvider failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
172
|
return '';
|
|
66
173
|
});
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
174
|
+
const config = await this.providerConfigStore.read().catch(() => null);
|
|
175
|
+
const providers = await Promise.all(definitions.map(async (def) => {
|
|
176
|
+
const providerConfig = config?.providers[def.id];
|
|
177
|
+
const authMethod = providerConfig?.authMethod;
|
|
178
|
+
return {
|
|
179
|
+
apiKeyUrl: def.apiKeyUrl,
|
|
180
|
+
authMethod,
|
|
181
|
+
category: def.category,
|
|
182
|
+
description: def.description,
|
|
183
|
+
id: def.id,
|
|
184
|
+
isConnected: await this.providerConfigStore.isProviderConnected(def.id).catch((error) => {
|
|
185
|
+
processLog(`[ProviderHandler] isProviderConnected failed for ${def.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
186
|
+
return false;
|
|
187
|
+
}),
|
|
188
|
+
isCurrent: def.id === activeProviderId,
|
|
189
|
+
name: def.name,
|
|
190
|
+
oauthCallbackMode: def.oauth?.callbackMode,
|
|
191
|
+
oauthLabel: def.oauth?.modes[0]?.label,
|
|
192
|
+
requiresApiKey: providerRequiresApiKey(def.id, authMethod),
|
|
193
|
+
supportsOAuth: Boolean(def.oauth),
|
|
194
|
+
};
|
|
195
|
+
}));
|
|
80
196
|
return { providers };
|
|
81
197
|
});
|
|
82
198
|
}
|
|
@@ -87,6 +203,92 @@ export class ProviderHandler {
|
|
|
87
203
|
return { success: true };
|
|
88
204
|
});
|
|
89
205
|
}
|
|
206
|
+
/* eslint-disable camelcase -- OAuth query params follow RFC 6749 naming */
|
|
207
|
+
setupStartOAuth() {
|
|
208
|
+
this.transport.onRequest(ProviderEvents.START_OAUTH, async (data, clientId) => {
|
|
209
|
+
const providerDef = getProviderById(data.providerId);
|
|
210
|
+
if (!providerDef?.oauth) {
|
|
211
|
+
const errorResponse = {
|
|
212
|
+
authUrl: '',
|
|
213
|
+
callbackMode: 'auto',
|
|
214
|
+
error: 'Provider does not support OAuth',
|
|
215
|
+
success: false,
|
|
216
|
+
};
|
|
217
|
+
return errorResponse;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const oauthConfig = providerDef.oauth;
|
|
221
|
+
// Clean up any existing flow for this provider (race condition guard)
|
|
222
|
+
const existingFlow = this.oauthFlows.get(data.providerId);
|
|
223
|
+
if (existingFlow?.callbackServer) {
|
|
224
|
+
await existingFlow.callbackServer.stop().catch(() => { });
|
|
225
|
+
}
|
|
226
|
+
this.oauthFlows.delete(data.providerId);
|
|
227
|
+
// Generate PKCE parameters
|
|
228
|
+
const pkce = this.generatePkce();
|
|
229
|
+
// Build auth URL
|
|
230
|
+
const mode = oauthConfig.modes.find((m) => m.id === (data.mode ?? 'default')) ?? oauthConfig.modes[0];
|
|
231
|
+
const params = new URLSearchParams({
|
|
232
|
+
client_id: oauthConfig.clientId,
|
|
233
|
+
code_challenge: pkce.codeChallenge,
|
|
234
|
+
code_challenge_method: 'S256',
|
|
235
|
+
redirect_uri: oauthConfig.redirectUri,
|
|
236
|
+
response_type: 'code',
|
|
237
|
+
scope: oauthConfig.scopes,
|
|
238
|
+
state: pkce.state,
|
|
239
|
+
});
|
|
240
|
+
// Provider-specific extra params (e.g. OpenAI's codex_cli_simplified_flow)
|
|
241
|
+
if (oauthConfig.extraParams) {
|
|
242
|
+
for (const [key, value] of Object.entries(oauthConfig.extraParams)) {
|
|
243
|
+
params.set(key, value);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const authUrl = `${mode.authUrl}?${params.toString()}`;
|
|
247
|
+
// Start callback server for auto mode
|
|
248
|
+
let callbackServer;
|
|
249
|
+
if (oauthConfig.callbackMode === 'auto' && oauthConfig.callbackPort) {
|
|
250
|
+
callbackServer = this.createCallbackServer({ port: oauthConfig.callbackPort });
|
|
251
|
+
await callbackServer.start();
|
|
252
|
+
}
|
|
253
|
+
// Store flow state
|
|
254
|
+
this.oauthFlows.set(data.providerId, {
|
|
255
|
+
callbackServer,
|
|
256
|
+
clientId,
|
|
257
|
+
codeVerifier: pkce.codeVerifier,
|
|
258
|
+
state: pkce.state,
|
|
259
|
+
});
|
|
260
|
+
// Open browser (non-fatal on failure)
|
|
261
|
+
try {
|
|
262
|
+
await this.browserLauncher.open(authUrl);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
processLog(`[ProviderHandler] Browser launch failed for OAuth — user can copy the URL`);
|
|
266
|
+
}
|
|
267
|
+
return { authUrl, callbackMode: oauthConfig.callbackMode, success: true };
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
// Clean up callback server if it was started but flow setup failed
|
|
271
|
+
const partialFlow = this.oauthFlows.get(data.providerId);
|
|
272
|
+
if (partialFlow?.callbackServer) {
|
|
273
|
+
await partialFlow.callbackServer.stop().catch(() => { });
|
|
274
|
+
}
|
|
275
|
+
this.oauthFlows.delete(data.providerId);
|
|
276
|
+
const errorResponse = {
|
|
277
|
+
authUrl: '',
|
|
278
|
+
callbackMode: 'auto',
|
|
279
|
+
error: getErrorMessage(error),
|
|
280
|
+
success: false,
|
|
281
|
+
};
|
|
282
|
+
return errorResponse;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
/* eslint-enable camelcase */
|
|
287
|
+
setupSubmitOAuthCode() {
|
|
288
|
+
this.transport.onRequest(ProviderEvents.SUBMIT_OAUTH_CODE,
|
|
289
|
+
// Stub for M2 (Anthropic code-paste flow)
|
|
290
|
+
async () => ({ error: 'Code submission is not yet supported for this provider', success: false }));
|
|
291
|
+
}
|
|
90
292
|
setupValidateApiKey() {
|
|
91
293
|
this.transport.onRequest(ProviderEvents.VALIDATE_API_KEY, async (data) => {
|
|
92
294
|
try {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT OAuth (Codex) API base URL — single source of truth.
|
|
3
|
+
* Used by the agent's OpenAI provider module and the server's provider config resolver.
|
|
4
|
+
*/
|
|
5
|
+
export declare const CHATGPT_OAUTH_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
6
|
+
/**
|
|
7
|
+
* Originator header/param value sent to OpenAI in OAuth flows.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CHATGPT_OAUTH_ORIGINATOR = "byterover";
|
|
10
|
+
/**
|
|
11
|
+
* OAuth callback timeout in milliseconds (5 minutes).
|
|
12
|
+
* Used by the callback server, TUI await-oauth-callback mutation, and CLI connect command.
|
|
13
|
+
*/
|
|
14
|
+
export declare const OAUTH_CALLBACK_TIMEOUT_MS = 300000;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT OAuth (Codex) API base URL — single source of truth.
|
|
3
|
+
* Used by the agent's OpenAI provider module and the server's provider config resolver.
|
|
4
|
+
*/
|
|
5
|
+
export const CHATGPT_OAUTH_BASE_URL = 'https://chatgpt.com/backend-api/codex';
|
|
6
|
+
/**
|
|
7
|
+
* Originator header/param value sent to OpenAI in OAuth flows.
|
|
8
|
+
*/
|
|
9
|
+
export const CHATGPT_OAUTH_ORIGINATOR = 'byterover';
|
|
10
|
+
/**
|
|
11
|
+
* OAuth callback timeout in milliseconds (5 minutes).
|
|
12
|
+
* Used by the callback server, TUI await-oauth-callback mutation, and CLI connect command.
|
|
13
|
+
*/
|
|
14
|
+
export const OAUTH_CALLBACK_TIMEOUT_MS = 300_000;
|
|
@@ -86,11 +86,16 @@ export declare const AllEventGroups: readonly [{
|
|
|
86
86
|
readonly COMPLETE: "onboarding:complete";
|
|
87
87
|
readonly GET_STATE: "onboarding:getState";
|
|
88
88
|
}, {
|
|
89
|
+
readonly AWAIT_OAUTH_CALLBACK: "provider:awaitOAuthCallback";
|
|
90
|
+
readonly CANCEL_OAUTH: "provider:cancelOAuth";
|
|
89
91
|
readonly CONNECT: "provider:connect";
|
|
90
92
|
readonly DISCONNECT: "provider:disconnect";
|
|
91
93
|
readonly GET_ACTIVE: "provider:getActive";
|
|
92
94
|
readonly LIST: "provider:list";
|
|
93
95
|
readonly SET_ACTIVE: "provider:setActive";
|
|
96
|
+
readonly START_OAUTH: "provider:startOAuth";
|
|
97
|
+
readonly SUBMIT_OAUTH_CODE: "provider:submitOAuthCode";
|
|
98
|
+
readonly UPDATED: "provider:updated";
|
|
94
99
|
readonly VALIDATE_API_KEY: "provider:validateApiKey";
|
|
95
100
|
}, {
|
|
96
101
|
readonly EXECUTE: "pull:execute";
|
|
@@ -9,6 +9,7 @@ export interface ModelListRequest {
|
|
|
9
9
|
}
|
|
10
10
|
export interface ModelListResponse {
|
|
11
11
|
activeModel?: string;
|
|
12
|
+
error?: string;
|
|
12
13
|
favorites: string[];
|
|
13
14
|
models: ModelDTO[];
|
|
14
15
|
recent: string[];
|
|
@@ -26,5 +27,6 @@ export interface ModelSetActiveRequest {
|
|
|
26
27
|
providerId: string;
|
|
27
28
|
}
|
|
28
29
|
export interface ModelSetActiveResponse {
|
|
30
|
+
error?: string;
|
|
29
31
|
success: boolean;
|
|
30
32
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { ProviderDTO } from '../types/dto.js';
|
|
2
2
|
export declare const ProviderEvents: {
|
|
3
|
+
readonly AWAIT_OAUTH_CALLBACK: "provider:awaitOAuthCallback";
|
|
4
|
+
readonly CANCEL_OAUTH: "provider:cancelOAuth";
|
|
3
5
|
readonly CONNECT: "provider:connect";
|
|
4
6
|
readonly DISCONNECT: "provider:disconnect";
|
|
5
7
|
readonly GET_ACTIVE: "provider:getActive";
|
|
6
8
|
readonly LIST: "provider:list";
|
|
7
9
|
readonly SET_ACTIVE: "provider:setActive";
|
|
10
|
+
readonly START_OAUTH: "provider:startOAuth";
|
|
11
|
+
readonly SUBMIT_OAUTH_CODE: "provider:submitOAuthCode";
|
|
12
|
+
readonly UPDATED: "provider:updated";
|
|
8
13
|
readonly VALIDATE_API_KEY: "provider:validateApiKey";
|
|
9
14
|
};
|
|
10
15
|
export interface ProviderListResponse {
|
|
@@ -42,3 +47,34 @@ export interface ProviderSetActiveRequest {
|
|
|
42
47
|
export interface ProviderSetActiveResponse {
|
|
43
48
|
success: boolean;
|
|
44
49
|
}
|
|
50
|
+
export interface ProviderCancelOAuthRequest {
|
|
51
|
+
providerId: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ProviderCancelOAuthResponse {
|
|
54
|
+
success: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface ProviderStartOAuthRequest {
|
|
57
|
+
mode?: string;
|
|
58
|
+
providerId: string;
|
|
59
|
+
}
|
|
60
|
+
export interface ProviderStartOAuthResponse {
|
|
61
|
+
authUrl: string;
|
|
62
|
+
callbackMode: 'auto' | 'code-paste';
|
|
63
|
+
error?: string;
|
|
64
|
+
success: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface ProviderAwaitOAuthCallbackRequest {
|
|
67
|
+
providerId: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ProviderAwaitOAuthCallbackResponse {
|
|
70
|
+
error?: string;
|
|
71
|
+
success: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface ProviderSubmitOAuthCodeRequest {
|
|
74
|
+
code: string;
|
|
75
|
+
providerId: string;
|
|
76
|
+
}
|
|
77
|
+
export interface ProviderSubmitOAuthCodeResponse {
|
|
78
|
+
error?: string;
|
|
79
|
+
success: boolean;
|
|
80
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export const ProviderEvents = {
|
|
2
|
+
AWAIT_OAUTH_CALLBACK: 'provider:awaitOAuthCallback',
|
|
3
|
+
CANCEL_OAUTH: 'provider:cancelOAuth',
|
|
2
4
|
CONNECT: 'provider:connect',
|
|
3
5
|
DISCONNECT: 'provider:disconnect',
|
|
4
6
|
GET_ACTIVE: 'provider:getActive',
|
|
5
7
|
LIST: 'provider:list',
|
|
6
8
|
SET_ACTIVE: 'provider:setActive',
|
|
9
|
+
START_OAUTH: 'provider:startOAuth',
|
|
10
|
+
SUBMIT_OAUTH_CODE: 'provider:submitOAuthCode',
|
|
11
|
+
UPDATED: 'provider:updated',
|
|
7
12
|
VALIDATE_API_KEY: 'provider:validateApiKey',
|
|
8
13
|
};
|
|
@@ -49,13 +49,17 @@ export interface ConnectorDTO {
|
|
|
49
49
|
}
|
|
50
50
|
export interface ProviderDTO {
|
|
51
51
|
apiKeyUrl?: string;
|
|
52
|
+
authMethod?: 'api-key' | 'oauth';
|
|
52
53
|
category: 'other' | 'popular';
|
|
53
54
|
description: string;
|
|
54
55
|
id: string;
|
|
55
56
|
isConnected: boolean;
|
|
56
57
|
isCurrent: boolean;
|
|
57
58
|
name: string;
|
|
59
|
+
oauthCallbackMode?: 'auto' | 'code-paste';
|
|
60
|
+
oauthLabel?: string;
|
|
58
61
|
requiresApiKey: boolean;
|
|
62
|
+
supportsOAuth: boolean;
|
|
59
63
|
}
|
|
60
64
|
export interface ModelDTO {
|
|
61
65
|
contextLength: number;
|
|
@@ -5,7 +5,7 @@ export type SetActiveModelDTO = {
|
|
|
5
5
|
modelId: string;
|
|
6
6
|
providerId: string;
|
|
7
7
|
};
|
|
8
|
-
export declare const setActiveModel: ({ contextLength, modelId, providerId }: SetActiveModelDTO) => Promise<ModelSetActiveResponse>;
|
|
8
|
+
export declare const setActiveModel: ({ contextLength, modelId, providerId, }: SetActiveModelDTO) => Promise<ModelSetActiveResponse>;
|
|
9
9
|
type UseSetActiveModelOptions = {
|
|
10
10
|
mutationConfig?: MutationConfig<typeof setActiveModel>;
|
|
11
11
|
};
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { useMutation } from '@tanstack/react-query';
|
|
2
|
-
import { ModelEvents } from '../../../../shared/transport/events/index.js';
|
|
2
|
+
import { ModelEvents, } from '../../../../shared/transport/events/index.js';
|
|
3
3
|
import { useTransportStore } from '../../../stores/transport-store.js';
|
|
4
|
-
export const setActiveModel = ({ contextLength, modelId, providerId }) => {
|
|
4
|
+
export const setActiveModel = async ({ contextLength, modelId, providerId, }) => {
|
|
5
5
|
const { apiClient } = useTransportStore.getState();
|
|
6
6
|
if (!apiClient)
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
throw new Error('Not connected');
|
|
8
|
+
const response = await apiClient.request(ModelEvents.SET_ACTIVE, {
|
|
9
|
+
contextLength,
|
|
10
|
+
modelId,
|
|
11
|
+
providerId,
|
|
12
|
+
});
|
|
13
|
+
if (!response.success && response.error) {
|
|
14
|
+
throw new Error(response.error);
|
|
15
|
+
}
|
|
16
|
+
return response;
|
|
9
17
|
};
|
|
10
18
|
export const useSetActiveModel = ({ mutationConfig } = {}) => useMutation({
|
|
11
19
|
...mutationConfig,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MutationConfig } from '../../../lib/react-query.js';
|
|
2
|
+
import { type ProviderAwaitOAuthCallbackResponse } from '../../../../shared/transport/events/index.js';
|
|
3
|
+
export type AwaitOAuthCallbackDTO = {
|
|
4
|
+
providerId: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const awaitOAuthCallback: ({ providerId, }: AwaitOAuthCallbackDTO) => Promise<ProviderAwaitOAuthCallbackResponse>;
|
|
7
|
+
type UseAwaitOAuthCallbackOptions = {
|
|
8
|
+
mutationConfig?: MutationConfig<typeof awaitOAuthCallback>;
|
|
9
|
+
};
|
|
10
|
+
export declare const useAwaitOAuthCallback: ({ mutationConfig }?: UseAwaitOAuthCallbackOptions) => import("@tanstack/react-query").UseMutationResult<ProviderAwaitOAuthCallbackResponse, Error, AwaitOAuthCallbackDTO, unknown>;
|
|
11
|
+
export {};
|