@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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/README.md +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +64 -7
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-store.ts +2 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +19 -7
- package/src/server/auth-cors.ts +114 -24
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/oauth/kiro.ts
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Kiro (AWS CodeWhisperer) OAuth — import-first.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* manual access-token paste (CLI only). Refresh hits the Kiro desktop refresh endpoint.
|
|
4
|
+
* Normal login imports the locally installed kiro-cli session. Account-add login deliberately
|
|
5
|
+
* asks kiro-cli to switch identities in its supported browser flow, then imports that fresh session.
|
|
7
6
|
*
|
|
8
7
|
* Ported from jawcode packages/ai/src/providers/kiro.ts (readKiroCliSqlite, refreshKiroDesktopToken).
|
|
9
|
-
* profileArn/region are
|
|
10
|
-
*
|
|
11
|
-
* only the access token.
|
|
8
|
+
* profileArn/region/client registration are persisted per OCX account so switching the account pool
|
|
9
|
+
* never combines one account's access token with another account's local Kiro profile metadata.
|
|
12
10
|
*/
|
|
13
|
-
import type { OAuthController, OAuthCredentials } from "./types";
|
|
11
|
+
import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
|
|
14
12
|
import {
|
|
13
|
+
discardKiroCliSessionRecovery,
|
|
15
14
|
inferRegionFromProfileArn,
|
|
16
15
|
inspectKiroCliSqliteSources,
|
|
16
|
+
inspectKiroCliSessionSnapshot,
|
|
17
17
|
normalizeKiroRegion,
|
|
18
|
+
persistKiroCliSessionRecovery,
|
|
18
19
|
readImportedKiroCredential,
|
|
19
20
|
readKiroCliSqliteCredential,
|
|
21
|
+
restoreKiroCliSession,
|
|
22
|
+
restoreStaleKiroCliSessionRecovery,
|
|
20
23
|
requireKiroRegion,
|
|
24
|
+
type ImportedKiroCredential,
|
|
25
|
+
type KiroCliSessionSnapshot,
|
|
21
26
|
type KiroImportDiagnostic,
|
|
22
27
|
} from "./kiro-credentials";
|
|
28
|
+
import { getAccountSet, saveAccountCredential } from "./store";
|
|
23
29
|
|
|
24
30
|
const DEFAULT_REGION = "us-east-1";
|
|
25
31
|
const REFRESH_URL = "https://prod.{region}.auth.desktop.kiro.dev/refreshToken";
|
|
26
32
|
const OIDC_URL = "https://oidc.{region}.amazonaws.com/token";
|
|
33
|
+
const KIRO_TERMINAL_REFRESH_ERRORS = new Set([
|
|
34
|
+
"invalid_grant",
|
|
35
|
+
"refresh_token_reused",
|
|
36
|
+
"revoked",
|
|
37
|
+
"revoked_token",
|
|
38
|
+
"refresh_token_revoked",
|
|
39
|
+
"access_denied",
|
|
40
|
+
"expired_token",
|
|
41
|
+
]);
|
|
27
42
|
|
|
28
43
|
interface ImportedKiroToken {
|
|
29
44
|
access: string;
|
|
@@ -31,6 +46,205 @@ interface ImportedKiroToken {
|
|
|
31
46
|
expires: number;
|
|
32
47
|
}
|
|
33
48
|
|
|
49
|
+
export interface KiroCliCommandResult {
|
|
50
|
+
exitCode: number;
|
|
51
|
+
stdout: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class KiroTokenRefreshError extends Error {
|
|
55
|
+
constructor(
|
|
56
|
+
readonly httpStatus: number,
|
|
57
|
+
readonly oauthError?: string,
|
|
58
|
+
) {
|
|
59
|
+
super(`Kiro token refresh failed: ${httpStatus}${oauthError ? ` (${oauthError})` : ""}`);
|
|
60
|
+
this.name = "KiroTokenRefreshError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type KiroCliRunner = (args: string[], signal?: AbortSignal) => Promise<KiroCliCommandResult>;
|
|
65
|
+
|
|
66
|
+
export interface KiroLoginOptions {
|
|
67
|
+
forceLogin?: boolean;
|
|
68
|
+
cliRunner?: KiroCliRunner;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pendingKiroLoginTransactions = new WeakMap<OAuthCredentials, KiroCliSessionSnapshot>();
|
|
72
|
+
/** Forced logins that started with no native CLI DB must logout on persistence failure. */
|
|
73
|
+
const pendingKiroEmptyPriorSessions = new WeakSet<OAuthCredentials>();
|
|
74
|
+
|
|
75
|
+
function logoutKiroCliBestEffort(): void {
|
|
76
|
+
try {
|
|
77
|
+
Bun.spawnSync(["kiro-cli", "logout"], {
|
|
78
|
+
stdin: "ignore",
|
|
79
|
+
stdout: "ignore",
|
|
80
|
+
stderr: "ignore",
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
// Best-effort rollback when the prior CLI state was empty.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Settle the external CLI side of a forced login after OCX credential persistence resolves. */
|
|
89
|
+
export function settleKiroLoginTransaction(credential: OAuthCredentials, persisted: boolean): void {
|
|
90
|
+
const snapshot = pendingKiroLoginTransactions.get(credential);
|
|
91
|
+
const emptyPrior = pendingKiroEmptyPriorSessions.has(credential);
|
|
92
|
+
pendingKiroLoginTransactions.delete(credential);
|
|
93
|
+
pendingKiroEmptyPriorSessions.delete(credential);
|
|
94
|
+
if (!persisted) {
|
|
95
|
+
if (snapshot) {
|
|
96
|
+
restoreKiroCliSession(snapshot);
|
|
97
|
+
discardKiroCliSessionRecovery(snapshot);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (emptyPrior) logoutKiroCliBestEffort();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (snapshot) discardKiroCliSessionRecovery(snapshot);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function restoreKiroLoginOrThrow(snapshot: KiroCliSessionSnapshot | null, emptyPrior: boolean, cause: unknown): never {
|
|
107
|
+
if (snapshot) {
|
|
108
|
+
try {
|
|
109
|
+
restoreKiroCliSession(snapshot);
|
|
110
|
+
discardKiroCliSessionRecovery(snapshot);
|
|
111
|
+
} catch (restoreError) {
|
|
112
|
+
throw new AggregateError(
|
|
113
|
+
[cause, restoreError],
|
|
114
|
+
"Kiro login failed and the previous Kiro CLI session could not be restored.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
} else if (emptyPrior) {
|
|
118
|
+
logoutKiroCliBestEffort();
|
|
119
|
+
}
|
|
120
|
+
throw cause;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function throwIfKiroLoginCancelled(signal?: AbortSignal): void {
|
|
124
|
+
if (signal?.aborted) throw new Error("Kiro login cancelled.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promise<KiroCliCommandResult> {
|
|
128
|
+
throwIfKiroLoginCancelled(signal);
|
|
129
|
+
let child: ReturnType<typeof Bun.spawn>;
|
|
130
|
+
try {
|
|
131
|
+
child = Bun.spawn(["kiro-cli", ...args], {
|
|
132
|
+
stdin: "ignore",
|
|
133
|
+
stdout: "pipe",
|
|
134
|
+
stderr: "ignore",
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
throw new Error("Kiro CLI is not installed or could not be started.");
|
|
139
|
+
}
|
|
140
|
+
const abort = () => child.kill();
|
|
141
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
142
|
+
// AbortSignal does not replay an abort that lands between the pre-check and listener registration.
|
|
143
|
+
if (signal?.aborted) abort();
|
|
144
|
+
try {
|
|
145
|
+
const [exitCode, stdout] = await Promise.all([
|
|
146
|
+
child.exited,
|
|
147
|
+
child.stdout instanceof ReadableStream ? new Response(child.stdout).text() : Promise.resolve(""),
|
|
148
|
+
]);
|
|
149
|
+
throwIfKiroLoginCancelled(signal);
|
|
150
|
+
return { exitCode, stdout };
|
|
151
|
+
} finally {
|
|
152
|
+
signal?.removeEventListener("abort", abort);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string }> {
|
|
157
|
+
try {
|
|
158
|
+
const result = await runner(["whoami", "--format", "json"], signal);
|
|
159
|
+
if (result.exitCode !== 0) return {};
|
|
160
|
+
const parsed = JSON.parse(result.stdout) as { email?: unknown };
|
|
161
|
+
const email = typeof parsed.email === "string" ? parsed.email.trim().toLowerCase() : "";
|
|
162
|
+
return email && email.length <= 320 ? { email } : {};
|
|
163
|
+
} catch {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function metadataFromImported(imported: ImportedKiroCredential): KiroOAuthMetadata | undefined {
|
|
169
|
+
const metadata: KiroOAuthMetadata = {
|
|
170
|
+
...(imported.profileArn ? { profileArn: imported.profileArn } : {}),
|
|
171
|
+
...(imported.ssoRegion ? { ssoRegion: imported.ssoRegion } : {}),
|
|
172
|
+
...(imported.apiRegion ? { apiRegion: imported.apiRegion } : {}),
|
|
173
|
+
...(imported.clientId ? { clientId: imported.clientId } : {}),
|
|
174
|
+
...(imported.clientSecret ? { clientSecret: imported.clientSecret } : {}),
|
|
175
|
+
};
|
|
176
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Persistable routing subset from explicit KIRO_* environment overrides (never local CLI state). */
|
|
180
|
+
export function environmentKiroRoutingMetadata(): Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion"> | undefined {
|
|
181
|
+
const profileArn = process.env.KIRO_PROFILE_ARN?.trim() || undefined;
|
|
182
|
+
const apiRegion = process.env.KIRO_API_REGION !== undefined
|
|
183
|
+
? requireKiroRegion(process.env.KIRO_API_REGION)
|
|
184
|
+
: undefined;
|
|
185
|
+
const ssoRegion = process.env.KIRO_REGION !== undefined
|
|
186
|
+
? requireKiroRegion(process.env.KIRO_REGION)
|
|
187
|
+
: undefined;
|
|
188
|
+
if (!profileArn && !apiRegion && !ssoRegion) return undefined;
|
|
189
|
+
return {
|
|
190
|
+
...(profileArn ? { profileArn } : {}),
|
|
191
|
+
...(apiRegion ? { apiRegion } : {}),
|
|
192
|
+
...(ssoRegion ? { ssoRegion } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Before Add-account switches the external CLI identity, bind any matching legacy identity-less
|
|
198
|
+
* OCX row to the current CLI session. Unmatched identity-less rows are left alone here so a
|
|
199
|
+
* cancelled browser login cannot destroy the only stored credential; after a successful add they
|
|
200
|
+
* remain selectable but cannot borrow the new CLI identity (and reauth refuses identity-less slots).
|
|
201
|
+
*/
|
|
202
|
+
async function bindMatchingLegacyIdentitylessKiroAccounts(
|
|
203
|
+
runner: KiroCliRunner,
|
|
204
|
+
signal?: AbortSignal,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const set = getAccountSet("kiro");
|
|
207
|
+
if (!set) return;
|
|
208
|
+
const legacyAccounts = set.accounts.filter(
|
|
209
|
+
account => account.credential.accountId === undefined && account.credential.email === undefined,
|
|
210
|
+
);
|
|
211
|
+
if (legacyAccounts.length === 0) return;
|
|
212
|
+
|
|
213
|
+
let imported: ImportedKiroCredential | null = null;
|
|
214
|
+
try {
|
|
215
|
+
imported = readKiroCliSqliteCredential();
|
|
216
|
+
} catch {
|
|
217
|
+
imported = null;
|
|
218
|
+
}
|
|
219
|
+
if (!imported?.refresh) return;
|
|
220
|
+
|
|
221
|
+
for (const account of legacyAccounts) {
|
|
222
|
+
const refresh = account.credential.refresh;
|
|
223
|
+
if (!refresh || imported.refresh !== refresh) continue;
|
|
224
|
+
const bound = await oauthCredentialFromImported(imported, runner, signal);
|
|
225
|
+
if (!bound.accountId && !bound.email) continue;
|
|
226
|
+
await saveAccountCredential("kiro", account.id, bound);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function oauthCredentialFromImported(
|
|
231
|
+
imported: ImportedKiroCredential,
|
|
232
|
+
runner: KiroCliRunner,
|
|
233
|
+
signal?: AbortSignal,
|
|
234
|
+
): Promise<OAuthCredentials> {
|
|
235
|
+
const identity = imported.source === "sqlite" ? await readKiroCliIdentity(runner, signal) : {};
|
|
236
|
+
const metadata = metadataFromImported(imported);
|
|
237
|
+
return {
|
|
238
|
+
access: imported.access,
|
|
239
|
+
refresh: imported.refresh,
|
|
240
|
+
expires: imported.expires,
|
|
241
|
+
source: imported.source === "json" ? "credential-file" : "local-cli",
|
|
242
|
+
...(imported.profileArn ? { accountId: imported.profileArn } : {}),
|
|
243
|
+
...(identity.email ? { email: identity.email } : {}),
|
|
244
|
+
...(metadata ? { kiro: metadata } : {}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
34
248
|
export type KiroCliImportDiagnosticStatus = KiroImportDiagnostic["status"];
|
|
35
249
|
export type KiroCliImportDiagnostic = KiroImportDiagnostic;
|
|
36
250
|
|
|
@@ -54,22 +268,74 @@ export function readKiroCliSqlite(): ImportedKiroToken | null {
|
|
|
54
268
|
* so the GUI renders the paste-input field, then blocks on onManualCodeInput for the token.
|
|
55
269
|
* If neither onAuth nor onManualCodeInput is available, throws a clear error.
|
|
56
270
|
*/
|
|
57
|
-
export async function loginKiro(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
271
|
+
export async function loginKiro(ctrl: OAuthController, options: KiroLoginOptions = {}): Promise<OAuthCredentials> {
|
|
272
|
+
const runner = options.cliRunner ?? defaultKiroCliRunner;
|
|
273
|
+
// A prior process may have exited after switching the external CLI account but before
|
|
274
|
+
// settlement. Recover that durable transaction before either importing or switching again.
|
|
275
|
+
restoreStaleKiroCliSessionRecovery();
|
|
276
|
+
if (options.forceLogin) {
|
|
277
|
+
throwIfKiroLoginCancelled(ctrl.signal);
|
|
278
|
+
ctrl.onAuth?.({
|
|
279
|
+
url: "",
|
|
280
|
+
instructions: "Kiro CLI is opening a fresh browser login. This also switches the account used by kiro-cli.",
|
|
281
|
+
});
|
|
282
|
+
ctrl.onProgress?.("Opening a fresh Kiro CLI browser login.");
|
|
283
|
+
// Never log out of a session we could not capture: the recovery contract promises an exact
|
|
284
|
+
// restore, so an uncapturable store (unreadable / schema-mismatched / ambiguous token) must
|
|
285
|
+
// abort here rather than let a later failure destroy it permanently.
|
|
286
|
+
const inspected = inspectKiroCliSessionSnapshot();
|
|
287
|
+
if (inspected.blocked) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
"Kiro CLI session could not be backed up, so OCX will not sign it out. " +
|
|
290
|
+
"Repair or remove the unreadable kiro-cli credential database " +
|
|
291
|
+
"(usually `~/.local/share/kiro-cli/data.sqlite3` or " +
|
|
292
|
+
"`~/Library/Application Support/kiro-cli/data.sqlite3`, or " +
|
|
293
|
+
"`%LOCALAPPDATA%\\Kiro-Cli\\data.sqlite3` on Windows), " +
|
|
294
|
+
"unset KIROCLI_DB_PATH / KIRO_CLI_DB_FILE if set for import-only overrides, then retry.",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const previousSession = inspected.snapshot;
|
|
298
|
+
await bindMatchingLegacyIdentitylessKiroAccounts(runner, ctrl.signal);
|
|
299
|
+
if (previousSession) persistKiroCliSessionRecovery(previousSession);
|
|
300
|
+
try {
|
|
301
|
+
const logout = await runner(["logout"], ctrl.signal);
|
|
302
|
+
throwIfKiroLoginCancelled(ctrl.signal);
|
|
303
|
+
if (logout.exitCode !== 0) throw new Error("Kiro CLI could not prepare a fresh login.");
|
|
304
|
+
const login = await runner(["login"], ctrl.signal);
|
|
305
|
+
throwIfKiroLoginCancelled(ctrl.signal);
|
|
306
|
+
if (login.exitCode !== 0) throw new Error("Kiro CLI login did not complete successfully.");
|
|
307
|
+
const fresh = readKiroCliSqliteCredential();
|
|
308
|
+
if (!fresh) throw new Error("Kiro CLI login completed but no credential could be imported.");
|
|
309
|
+
const credential = await oauthCredentialFromImported(fresh, runner, ctrl.signal);
|
|
310
|
+
throwIfKiroLoginCancelled(ctrl.signal);
|
|
311
|
+
if (!credential.accountId && !credential.email) {
|
|
312
|
+
throw new Error("Kiro login completed but OCX could not determine a stable account identity.");
|
|
313
|
+
}
|
|
314
|
+
if (previousSession) pendingKiroLoginTransactions.set(credential, previousSession);
|
|
315
|
+
else pendingKiroEmptyPriorSessions.add(credential);
|
|
316
|
+
return credential;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
restoreKiroLoginOrThrow(previousSession, !previousSession, error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
58
322
|
const imported = readImportedKiroCredential();
|
|
59
323
|
if (imported) {
|
|
60
324
|
ctrl.onProgress?.(imported.source === "json" ? "Imported token from Kiro credentials file." : "Imported token from installed kiro-cli login.");
|
|
61
|
-
return
|
|
62
|
-
access: imported.access,
|
|
63
|
-
refresh: imported.refresh,
|
|
64
|
-
expires: imported.expires,
|
|
65
|
-
source: imported.source === "json" ? "credential-file" : "local-cli",
|
|
66
|
-
};
|
|
325
|
+
return oauthCredentialFromImported(imported, runner, ctrl.signal);
|
|
67
326
|
}
|
|
68
327
|
|
|
69
328
|
const envToken = process.env.KIRO_ACCESS_TOKEN;
|
|
70
329
|
if (envToken) {
|
|
71
330
|
ctrl.onProgress?.("Using KIRO_ACCESS_TOKEN from environment.");
|
|
72
|
-
|
|
331
|
+
const routing = environmentKiroRoutingMetadata();
|
|
332
|
+
return {
|
|
333
|
+
access: envToken,
|
|
334
|
+
refresh: process.env.KIRO_REFRESH_TOKEN ?? "",
|
|
335
|
+
expires: Date.now() + 3600_000,
|
|
336
|
+
source: "environment",
|
|
337
|
+
...(routing ? { kiro: routing } : {}),
|
|
338
|
+
};
|
|
73
339
|
}
|
|
74
340
|
|
|
75
341
|
if (ctrl.onManualCodeInput) {
|
|
@@ -85,7 +351,16 @@ export async function loginKiro(ctrl: OAuthController): Promise<OAuthCredentials
|
|
|
85
351
|
});
|
|
86
352
|
ctrl.onProgress?.("No kiro-cli token found. Paste a Kiro access token (starts with 'aoa'), or install the Kiro CLI and run `kiro-cli login` first.");
|
|
87
353
|
const raw = (await ctrl.onManualCodeInput()).trim();
|
|
88
|
-
if (raw)
|
|
354
|
+
if (raw) {
|
|
355
|
+
const routing = environmentKiroRoutingMetadata();
|
|
356
|
+
return {
|
|
357
|
+
access: raw,
|
|
358
|
+
refresh: "",
|
|
359
|
+
expires: Date.now() + 3600_000,
|
|
360
|
+
source: "manual",
|
|
361
|
+
...(routing ? { kiro: routing } : {}),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
89
364
|
}
|
|
90
365
|
|
|
91
366
|
throw new Error(
|
|
@@ -95,16 +370,25 @@ export async function loginKiro(ctrl: OAuthController): Promise<OAuthCredentials
|
|
|
95
370
|
);
|
|
96
371
|
}
|
|
97
372
|
|
|
98
|
-
/**
|
|
99
|
-
export function resolveKiroRegion(): string {
|
|
373
|
+
/** Account metadata is authoritative; legacy accountless calls use KIRO_REGION → local import → default. */
|
|
374
|
+
export function resolveKiroRegion(account?: KiroOAuthMetadata): string {
|
|
375
|
+
if (account !== undefined) return normalizeKiroRegion(account.ssoRegion) || DEFAULT_REGION;
|
|
100
376
|
if (process.env.KIRO_REGION !== undefined) return requireKiroRegion(process.env.KIRO_REGION);
|
|
101
377
|
return normalizeKiroRegion(readImportedKiroCredential()?.ssoRegion) || DEFAULT_REGION;
|
|
102
378
|
}
|
|
103
379
|
|
|
104
|
-
/**
|
|
105
|
-
export function resolveKiroApiRegion(): string {
|
|
106
|
-
|
|
380
|
+
/** Account metadata is authoritative; legacy accountless calls may use KIRO_API_REGION/local import. */
|
|
381
|
+
export function resolveKiroApiRegion(account?: Pick<KiroOAuthMetadata, "apiRegion" | "profileArn" | "ssoRegion">): string {
|
|
382
|
+
if (account !== undefined) {
|
|
383
|
+
return (
|
|
384
|
+
normalizeKiroRegion(account.apiRegion) ||
|
|
385
|
+
inferRegionFromProfileArn(account.profileArn) ||
|
|
386
|
+
normalizeKiroRegion(account.ssoRegion) ||
|
|
387
|
+
DEFAULT_REGION
|
|
388
|
+
);
|
|
389
|
+
}
|
|
107
390
|
if (process.env.KIRO_API_REGION !== undefined) return requireKiroRegion(process.env.KIRO_API_REGION);
|
|
391
|
+
const imported = readImportedKiroCredential();
|
|
108
392
|
return (
|
|
109
393
|
normalizeKiroRegion(imported?.apiRegion) ||
|
|
110
394
|
inferRegionFromProfileArn(imported?.profileArn) ||
|
|
@@ -116,15 +400,29 @@ export function resolveKiroApiRegion(): string {
|
|
|
116
400
|
|
|
117
401
|
/**
|
|
118
402
|
* Resolve the CodeWhisperer profileArn for request-time use by the adapter.
|
|
119
|
-
*
|
|
120
|
-
* (the adapter decides whether that is fatal).
|
|
403
|
+
* Account metadata is authoritative. Legacy accountless calls use KIRO_PROFILE_ARN → local import.
|
|
404
|
+
* Returns undefined if absent (the adapter decides whether that is fatal).
|
|
121
405
|
*/
|
|
122
|
-
export function resolveKiroProfileArn(): string | undefined {
|
|
406
|
+
export function resolveKiroProfileArn(account?: Pick<KiroOAuthMetadata, "profileArn">): string | undefined {
|
|
407
|
+
if (account !== undefined) return account.profileArn;
|
|
123
408
|
const env = process.env.KIRO_PROFILE_ARN;
|
|
124
409
|
if (env) return env;
|
|
125
410
|
return readImportedKiroCredential()?.profileArn;
|
|
126
411
|
}
|
|
127
412
|
|
|
413
|
+
async function kiroTokenRefreshError(response: Response): Promise<KiroTokenRefreshError> {
|
|
414
|
+
let oauthError: string | undefined;
|
|
415
|
+
try {
|
|
416
|
+
const payload = await response.json() as { error?: unknown };
|
|
417
|
+
if (typeof payload.error === "string" && KIRO_TERMINAL_REFRESH_ERRORS.has(payload.error)) {
|
|
418
|
+
oauthError = payload.error;
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
// Error bodies are untrusted and intentionally excluded from the surfaced message.
|
|
422
|
+
}
|
|
423
|
+
return new KiroTokenRefreshError(response.status, oauthError);
|
|
424
|
+
}
|
|
425
|
+
|
|
128
426
|
async function readTokenResponse(res: Response, oldRefresh: string): Promise<OAuthCredentials> {
|
|
129
427
|
const data = (await res.json()) as { accessToken?: string; refreshToken?: string; expiresIn?: number };
|
|
130
428
|
if (!data.accessToken) throw new Error("Kiro refresh returned no accessToken");
|
|
@@ -135,43 +433,126 @@ async function readTokenResponse(res: Response, oldRefresh: string): Promise<OAu
|
|
|
135
433
|
};
|
|
136
434
|
}
|
|
137
435
|
|
|
138
|
-
|
|
139
|
-
const
|
|
436
|
+
function kiroRefreshSignal(signal?: AbortSignal): AbortSignal {
|
|
437
|
+
const timeout = AbortSignal.timeout(30_000);
|
|
438
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function refreshKiroDesktopToken(refresh: string, signal?: AbortSignal, metadata?: KiroOAuthMetadata): Promise<OAuthCredentials> {
|
|
442
|
+
const region = resolveKiroRegion(metadata);
|
|
140
443
|
const res = await fetch(REFRESH_URL.replace("{region}", region), {
|
|
141
444
|
method: "POST",
|
|
142
445
|
headers: { "Content-Type": "application/json" },
|
|
143
446
|
body: JSON.stringify({ refreshToken: refresh }),
|
|
144
|
-
signal: signal
|
|
447
|
+
signal: kiroRefreshSignal(signal),
|
|
145
448
|
});
|
|
146
|
-
if (!res.ok) throw
|
|
449
|
+
if (!res.ok) throw await kiroTokenRefreshError(res);
|
|
147
450
|
return readTokenResponse(res, refresh);
|
|
148
451
|
}
|
|
149
452
|
|
|
150
|
-
async function refreshAwsSsoOidcToken(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
453
|
+
async function refreshAwsSsoOidcToken(
|
|
454
|
+
refresh: string,
|
|
455
|
+
signal?: AbortSignal,
|
|
456
|
+
credential?: OAuthCredentials,
|
|
457
|
+
): Promise<OAuthCredentials> {
|
|
458
|
+
let metadata = credential?.kiro;
|
|
459
|
+
if (!metadata) {
|
|
460
|
+
const local = readImportedKiroCredential();
|
|
461
|
+
// Only a local store holding this exact refresh token describes this account. Anything else
|
|
462
|
+
// belongs to whichever account kiro-cli is currently signed into.
|
|
463
|
+
if (local?.refresh === refresh) metadata = metadataFromImported(local);
|
|
464
|
+
}
|
|
465
|
+
// A stored OCX account with no usable `kiro` metadata must still refresh account-scoped: falling
|
|
466
|
+
// through to `resolveKiroRegion(undefined)` would read KIRO_REGION or the local CLI import and
|
|
467
|
+
// borrow an unrelated account's region after a switch. Environment/manual credentials may still
|
|
468
|
+
// honor explicit KIRO_* routing; other stored accounts pin an empty marker (default region).
|
|
469
|
+
// Only a truly accountless refresh (no stored credential) keeps the legacy env/local fallback.
|
|
470
|
+
if (!metadata && credential) {
|
|
471
|
+
metadata = credential.source === "environment" || credential.source === "manual"
|
|
472
|
+
? environmentKiroRoutingMetadata() ?? {}
|
|
473
|
+
: {};
|
|
474
|
+
}
|
|
475
|
+
const clientId = metadata?.clientId;
|
|
476
|
+
const clientSecret = metadata?.clientSecret;
|
|
477
|
+
if (!clientId || !clientSecret) return refreshKiroDesktopToken(refresh, signal, metadata);
|
|
478
|
+
const region = resolveKiroRegion(metadata);
|
|
154
479
|
const run = async (refreshToken: string): Promise<Response> => fetch(OIDC_URL.replace("{region}", region), {
|
|
155
480
|
method: "POST",
|
|
156
481
|
headers: { "Content-Type": "application/json" },
|
|
157
482
|
body: JSON.stringify({
|
|
158
483
|
grantType: "refresh_token",
|
|
159
|
-
clientId
|
|
160
|
-
clientSecret
|
|
484
|
+
clientId,
|
|
485
|
+
clientSecret,
|
|
161
486
|
refreshToken,
|
|
162
487
|
}),
|
|
163
|
-
signal: signal
|
|
488
|
+
signal: kiroRefreshSignal(signal),
|
|
164
489
|
});
|
|
165
|
-
|
|
166
|
-
if (!res.ok
|
|
167
|
-
const reloaded = readImportedKiroCredential();
|
|
168
|
-
if (reloaded?.refresh && reloaded.refresh !== refresh) res = await run(reloaded.refresh);
|
|
169
|
-
}
|
|
170
|
-
if (!res.ok) throw new Error(`Kiro AWS SSO OIDC refresh failed: ${res.status}`);
|
|
490
|
+
const res = await run(refresh);
|
|
491
|
+
if (!res.ok) throw await kiroTokenRefreshError(res);
|
|
171
492
|
return readTokenResponse(res, refresh);
|
|
172
493
|
}
|
|
173
494
|
|
|
174
|
-
|
|
495
|
+
function matchingRotatedKiroCliCredential(
|
|
496
|
+
refresh: string,
|
|
497
|
+
credential?: OAuthCredentials,
|
|
498
|
+
): ImportedKiroCredential | undefined {
|
|
499
|
+
const storedIdentities = [credential?.kiro?.profileArn, credential?.accountId]
|
|
500
|
+
.filter((value): value is string => Boolean(value));
|
|
501
|
+
if (storedIdentities.length === 0 || new Set(storedIdentities).size !== 1) return undefined;
|
|
502
|
+
try {
|
|
503
|
+
const local = readKiroCliSqliteCredential();
|
|
504
|
+
if (!local?.profileArn || storedIdentities.some(identity => identity !== local.profileArn)) return undefined;
|
|
505
|
+
if (!local.refresh || local.refresh === refresh) return undefined;
|
|
506
|
+
return local;
|
|
507
|
+
} catch {
|
|
508
|
+
// An unrelated or malformed local store must not block the stored OCX account.
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function metadataForRotatedKiroCliCredential(
|
|
514
|
+
credential: OAuthCredentials,
|
|
515
|
+
local: ImportedKiroCredential,
|
|
516
|
+
): KiroOAuthMetadata {
|
|
517
|
+
const {
|
|
518
|
+
clientId: _storedClientId,
|
|
519
|
+
clientSecret: _storedClientSecret,
|
|
520
|
+
...storedRouting
|
|
521
|
+
} = credential.kiro ?? {};
|
|
522
|
+
const localMetadata = metadataFromImported(local) ?? {};
|
|
523
|
+
const {
|
|
524
|
+
clientId: _localClientId,
|
|
525
|
+
clientSecret: _localClientSecret,
|
|
526
|
+
...localRouting
|
|
527
|
+
} = localMetadata;
|
|
528
|
+
return {
|
|
529
|
+
...storedRouting,
|
|
530
|
+
...localRouting,
|
|
531
|
+
...(local.authType === "aws_sso_oidc" && local.clientId && local.clientSecret
|
|
532
|
+
? { clientId: local.clientId, clientSecret: local.clientSecret }
|
|
533
|
+
: {}),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export async function refreshKiroToken(
|
|
538
|
+
refresh: string,
|
|
539
|
+
signal?: AbortSignal,
|
|
540
|
+
credential?: OAuthCredentials,
|
|
541
|
+
): Promise<OAuthCredentials> {
|
|
175
542
|
if (!refresh) throw new Error("Kiro: no refresh token available (re-run `kiro-cli login`).");
|
|
176
|
-
|
|
543
|
+
try {
|
|
544
|
+
return await refreshAwsSsoOidcToken(refresh, signal, credential);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
if (!(error instanceof KiroTokenRefreshError) || error.httpStatus !== 400) throw error;
|
|
547
|
+
if (!credential) throw error;
|
|
548
|
+
const local = matchingRotatedKiroCliCredential(refresh, credential);
|
|
549
|
+
if (!local) throw error;
|
|
550
|
+
const retryMetadata = metadataForRotatedKiroCliCredential(credential, local);
|
|
551
|
+
const fresh = await refreshAwsSsoOidcToken(local.refresh, signal, {
|
|
552
|
+
...credential,
|
|
553
|
+
refresh: local.refresh,
|
|
554
|
+
kiro: retryMetadata,
|
|
555
|
+
});
|
|
556
|
+
return { ...fresh, kiro: retryMetadata };
|
|
557
|
+
}
|
|
177
558
|
}
|
package/src/oauth/login-cli.ts
CHANGED
|
@@ -2,19 +2,21 @@ import * as readline from "node:readline";
|
|
|
2
2
|
import { openUrl } from "../lib/open-url";
|
|
3
3
|
import { loadConfig, saveConfig } from "../config";
|
|
4
4
|
import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
|
|
5
|
-
import { isPublicOAuthProvider, listOAuthProviders,
|
|
5
|
+
import { isPublicOAuthProvider, listOAuthProviders, runLogin } from "./index";
|
|
6
6
|
import { KEY_LOGIN_PROVIDERS, isKeyLoginProvider, validateApiKey, type KeyLoginProvider } from "./key-providers";
|
|
7
7
|
import type { OcxProviderConfig } from "../types";
|
|
8
|
+
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
9
|
+
import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
|
|
8
10
|
|
|
9
11
|
export function runningProxyUpdateHeaders(): Headers {
|
|
10
12
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
13
|
+
const adminToken = configuredAdminToken();
|
|
14
|
+
if (adminToken) headers.set("X-OpenCodex-API-Key", adminToken);
|
|
13
15
|
return headers;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/** Push the new provider into a running proxy's live config so it routes without a restart. */
|
|
17
|
-
async function notifyRunningProxy(name: string, provider: unknown): Promise<void> {
|
|
19
|
+
export async function notifyRunningProxy(name: string, provider: unknown): Promise<void> {
|
|
18
20
|
// Identity-checked runtime-port lookup: reaches a fallback-port proxy and avoids
|
|
19
21
|
// posting credentials-adjacent config to whatever else answers on config.port.
|
|
20
22
|
const live = await findLiveProxy();
|
|
@@ -30,6 +32,19 @@ async function notifyRunningProxy(name: string, provider: unknown): Promise<void
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* After `runLogin()` has persisted the merged provider (including preserved apiKey /
|
|
37
|
+
* apiKeyPool / authMode), push that on-disk entry into a running proxy.
|
|
38
|
+
*
|
|
39
|
+
* Must not send `OAUTH_PROVIDERS[name].providerConfig`: POST /api/providers replaces the
|
|
40
|
+
* live entry and saves it, which would drop the preserved key billing state.
|
|
41
|
+
*/
|
|
42
|
+
export async function notifyRunningProxyAfterOAuthLogin(name: string): Promise<void> {
|
|
43
|
+
const provider = loadConfig().providers[name];
|
|
44
|
+
if (!provider) return;
|
|
45
|
+
await notifyRunningProxy(name, provider);
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
export async function handleLogin(provider?: string): Promise<void> {
|
|
34
49
|
const name = (provider ?? "").trim().toLowerCase();
|
|
35
50
|
if (isPublicOAuthProvider(name)) return handleOAuthLogin(name);
|
|
@@ -58,7 +73,7 @@ async function handleOAuthLogin(name: string): Promise<void> {
|
|
|
58
73
|
} finally {
|
|
59
74
|
rl.close();
|
|
60
75
|
}
|
|
61
|
-
await
|
|
76
|
+
await notifyRunningProxyAfterOAuthLogin(name);
|
|
62
77
|
console.log(`\n✅ Logged in to ${name}. Try: ocx sync`);
|
|
63
78
|
}
|
|
64
79
|
|
|
@@ -67,6 +82,7 @@ export function providerConfigFromKeyLoginProvider(def: KeyLoginProvider, key: s
|
|
|
67
82
|
adapter: def.adapter,
|
|
68
83
|
baseUrl: baseUrlOverride ?? def.baseUrl,
|
|
69
84
|
apiKey: key,
|
|
85
|
+
...(def.apiKeyTransport !== undefined ? { apiKeyTransport: def.apiKeyTransport } : {}),
|
|
70
86
|
...(def.defaultModel ? { defaultModel: def.defaultModel } : {}),
|
|
71
87
|
...(def.models ? { models: [...def.models] } : {}),
|
|
72
88
|
...(def.contextWindow !== undefined ? { contextWindow: def.contextWindow } : {}),
|
|
@@ -92,6 +108,12 @@ export function providerConfigFromKeyLoginProvider(def: KeyLoginProvider, key: s
|
|
|
92
108
|
|
|
93
109
|
async function handleKeyLogin(name: string): Promise<void> {
|
|
94
110
|
const def = KEY_LOGIN_PROVIDERS[name];
|
|
111
|
+
const preflightConfig = loadConfig();
|
|
112
|
+
const namespaceCollision = codexAccountNamespaceProviderCollisionError(preflightConfig.codexAccountNamespaces, name);
|
|
113
|
+
if (namespaceCollision) {
|
|
114
|
+
console.error(`Error: ${namespaceCollision}.`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
95
117
|
console.log(`\n🔑 ${def.label} — opening ${def.dashboardUrl} so you can create/copy an API key...`);
|
|
96
118
|
openUrl(def.dashboardUrl);
|
|
97
119
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -113,7 +135,7 @@ async function handleKeyLogin(name: string): Promise<void> {
|
|
|
113
135
|
process.exit(1);
|
|
114
136
|
}
|
|
115
137
|
process.stdout.write(" validating… ");
|
|
116
|
-
const valid = await validateApiKey({ ...def, baseUrl }, key);
|
|
138
|
+
const valid = await validateApiKey(name, { ...def, baseUrl }, key);
|
|
117
139
|
console.log(valid === true ? "valid ✅" : valid === false ? "INVALID ❌" : "couldn't validate (may still work)");
|
|
118
140
|
if (valid === false) {
|
|
119
141
|
console.error("Provider rejected the key. Not saved.");
|
|
@@ -121,6 +143,11 @@ async function handleKeyLogin(name: string): Promise<void> {
|
|
|
121
143
|
}
|
|
122
144
|
const provider = providerConfigFromKeyLoginProvider(def, key, baseUrl);
|
|
123
145
|
const config = loadConfig();
|
|
146
|
+
const commitCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, name);
|
|
147
|
+
if (commitCollision) {
|
|
148
|
+
console.error(`Error: ${commitCollision}.`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
124
151
|
config.providers[name] = provider;
|
|
125
152
|
saveConfig(config);
|
|
126
153
|
await notifyRunningProxy(name, provider);
|