@bitkyc08/opencodex 2.7.42 ā 2.7.43
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 +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -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/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 +49 -6
- 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/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -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 +55 -16
- package/src/cli/status.ts +47 -13
- 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-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/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- 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 +15 -0
- package/src/codex/routing.ts +695 -106
- 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 +566 -25
- 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/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- 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 +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -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 +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -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 +14 -4
- package/src/server/auth-cors.ts +110 -22
- 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/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- 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 +583 -152
- 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/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -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 +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- 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/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.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,73 @@ 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`), " +
|
|
293
|
+
"unset KIROCLI_DB_PATH / KIRO_CLI_DB_FILE if set for import-only overrides, then retry.",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const previousSession = inspected.snapshot;
|
|
297
|
+
await bindMatchingLegacyIdentitylessKiroAccounts(runner, ctrl.signal);
|
|
298
|
+
if (previousSession) persistKiroCliSessionRecovery(previousSession);
|
|
299
|
+
try {
|
|
300
|
+
const logout = await runner(["logout"], ctrl.signal);
|
|
301
|
+
throwIfKiroLoginCancelled(ctrl.signal);
|
|
302
|
+
if (logout.exitCode !== 0) throw new Error("Kiro CLI could not prepare a fresh login.");
|
|
303
|
+
const login = await runner(["login"], ctrl.signal);
|
|
304
|
+
throwIfKiroLoginCancelled(ctrl.signal);
|
|
305
|
+
if (login.exitCode !== 0) throw new Error("Kiro CLI login did not complete successfully.");
|
|
306
|
+
const fresh = readKiroCliSqliteCredential();
|
|
307
|
+
if (!fresh) throw new Error("Kiro CLI login completed but no credential could be imported.");
|
|
308
|
+
const credential = await oauthCredentialFromImported(fresh, runner, ctrl.signal);
|
|
309
|
+
throwIfKiroLoginCancelled(ctrl.signal);
|
|
310
|
+
if (!credential.accountId && !credential.email) {
|
|
311
|
+
throw new Error("Kiro login completed but OCX could not determine a stable account identity.");
|
|
312
|
+
}
|
|
313
|
+
if (previousSession) pendingKiroLoginTransactions.set(credential, previousSession);
|
|
314
|
+
else pendingKiroEmptyPriorSessions.add(credential);
|
|
315
|
+
return credential;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
restoreKiroLoginOrThrow(previousSession, !previousSession, error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
58
321
|
const imported = readImportedKiroCredential();
|
|
59
322
|
if (imported) {
|
|
60
323
|
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
|
-
};
|
|
324
|
+
return oauthCredentialFromImported(imported, runner, ctrl.signal);
|
|
67
325
|
}
|
|
68
326
|
|
|
69
327
|
const envToken = process.env.KIRO_ACCESS_TOKEN;
|
|
70
328
|
if (envToken) {
|
|
71
329
|
ctrl.onProgress?.("Using KIRO_ACCESS_TOKEN from environment.");
|
|
72
|
-
|
|
330
|
+
const routing = environmentKiroRoutingMetadata();
|
|
331
|
+
return {
|
|
332
|
+
access: envToken,
|
|
333
|
+
refresh: process.env.KIRO_REFRESH_TOKEN ?? "",
|
|
334
|
+
expires: Date.now() + 3600_000,
|
|
335
|
+
source: "environment",
|
|
336
|
+
...(routing ? { kiro: routing } : {}),
|
|
337
|
+
};
|
|
73
338
|
}
|
|
74
339
|
|
|
75
340
|
if (ctrl.onManualCodeInput) {
|
|
@@ -85,7 +350,16 @@ export async function loginKiro(ctrl: OAuthController): Promise<OAuthCredentials
|
|
|
85
350
|
});
|
|
86
351
|
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
352
|
const raw = (await ctrl.onManualCodeInput()).trim();
|
|
88
|
-
if (raw)
|
|
353
|
+
if (raw) {
|
|
354
|
+
const routing = environmentKiroRoutingMetadata();
|
|
355
|
+
return {
|
|
356
|
+
access: raw,
|
|
357
|
+
refresh: "",
|
|
358
|
+
expires: Date.now() + 3600_000,
|
|
359
|
+
source: "manual",
|
|
360
|
+
...(routing ? { kiro: routing } : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
89
363
|
}
|
|
90
364
|
|
|
91
365
|
throw new Error(
|
|
@@ -95,16 +369,25 @@ export async function loginKiro(ctrl: OAuthController): Promise<OAuthCredentials
|
|
|
95
369
|
);
|
|
96
370
|
}
|
|
97
371
|
|
|
98
|
-
/**
|
|
99
|
-
export function resolveKiroRegion(): string {
|
|
372
|
+
/** Account metadata is authoritative; legacy accountless calls use KIRO_REGION ā local import ā default. */
|
|
373
|
+
export function resolveKiroRegion(account?: KiroOAuthMetadata): string {
|
|
374
|
+
if (account !== undefined) return normalizeKiroRegion(account.ssoRegion) || DEFAULT_REGION;
|
|
100
375
|
if (process.env.KIRO_REGION !== undefined) return requireKiroRegion(process.env.KIRO_REGION);
|
|
101
376
|
return normalizeKiroRegion(readImportedKiroCredential()?.ssoRegion) || DEFAULT_REGION;
|
|
102
377
|
}
|
|
103
378
|
|
|
104
|
-
/**
|
|
105
|
-
export function resolveKiroApiRegion(): string {
|
|
106
|
-
|
|
379
|
+
/** Account metadata is authoritative; legacy accountless calls may use KIRO_API_REGION/local import. */
|
|
380
|
+
export function resolveKiroApiRegion(account?: Pick<KiroOAuthMetadata, "apiRegion" | "profileArn" | "ssoRegion">): string {
|
|
381
|
+
if (account !== undefined) {
|
|
382
|
+
return (
|
|
383
|
+
normalizeKiroRegion(account.apiRegion) ||
|
|
384
|
+
inferRegionFromProfileArn(account.profileArn) ||
|
|
385
|
+
normalizeKiroRegion(account.ssoRegion) ||
|
|
386
|
+
DEFAULT_REGION
|
|
387
|
+
);
|
|
388
|
+
}
|
|
107
389
|
if (process.env.KIRO_API_REGION !== undefined) return requireKiroRegion(process.env.KIRO_API_REGION);
|
|
390
|
+
const imported = readImportedKiroCredential();
|
|
108
391
|
return (
|
|
109
392
|
normalizeKiroRegion(imported?.apiRegion) ||
|
|
110
393
|
inferRegionFromProfileArn(imported?.profileArn) ||
|
|
@@ -116,15 +399,29 @@ export function resolveKiroApiRegion(): string {
|
|
|
116
399
|
|
|
117
400
|
/**
|
|
118
401
|
* Resolve the CodeWhisperer profileArn for request-time use by the adapter.
|
|
119
|
-
*
|
|
120
|
-
* (the adapter decides whether that is fatal).
|
|
402
|
+
* Account metadata is authoritative. Legacy accountless calls use KIRO_PROFILE_ARN ā local import.
|
|
403
|
+
* Returns undefined if absent (the adapter decides whether that is fatal).
|
|
121
404
|
*/
|
|
122
|
-
export function resolveKiroProfileArn(): string | undefined {
|
|
405
|
+
export function resolveKiroProfileArn(account?: Pick<KiroOAuthMetadata, "profileArn">): string | undefined {
|
|
406
|
+
if (account !== undefined) return account.profileArn;
|
|
123
407
|
const env = process.env.KIRO_PROFILE_ARN;
|
|
124
408
|
if (env) return env;
|
|
125
409
|
return readImportedKiroCredential()?.profileArn;
|
|
126
410
|
}
|
|
127
411
|
|
|
412
|
+
async function kiroTokenRefreshError(response: Response): Promise<KiroTokenRefreshError> {
|
|
413
|
+
let oauthError: string | undefined;
|
|
414
|
+
try {
|
|
415
|
+
const payload = await response.json() as { error?: unknown };
|
|
416
|
+
if (typeof payload.error === "string" && KIRO_TERMINAL_REFRESH_ERRORS.has(payload.error)) {
|
|
417
|
+
oauthError = payload.error;
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
// Error bodies are untrusted and intentionally excluded from the surfaced message.
|
|
421
|
+
}
|
|
422
|
+
return new KiroTokenRefreshError(response.status, oauthError);
|
|
423
|
+
}
|
|
424
|
+
|
|
128
425
|
async function readTokenResponse(res: Response, oldRefresh: string): Promise<OAuthCredentials> {
|
|
129
426
|
const data = (await res.json()) as { accessToken?: string; refreshToken?: string; expiresIn?: number };
|
|
130
427
|
if (!data.accessToken) throw new Error("Kiro refresh returned no accessToken");
|
|
@@ -135,43 +432,126 @@ async function readTokenResponse(res: Response, oldRefresh: string): Promise<OAu
|
|
|
135
432
|
};
|
|
136
433
|
}
|
|
137
434
|
|
|
138
|
-
|
|
139
|
-
const
|
|
435
|
+
function kiroRefreshSignal(signal?: AbortSignal): AbortSignal {
|
|
436
|
+
const timeout = AbortSignal.timeout(30_000);
|
|
437
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function refreshKiroDesktopToken(refresh: string, signal?: AbortSignal, metadata?: KiroOAuthMetadata): Promise<OAuthCredentials> {
|
|
441
|
+
const region = resolveKiroRegion(metadata);
|
|
140
442
|
const res = await fetch(REFRESH_URL.replace("{region}", region), {
|
|
141
443
|
method: "POST",
|
|
142
444
|
headers: { "Content-Type": "application/json" },
|
|
143
445
|
body: JSON.stringify({ refreshToken: refresh }),
|
|
144
|
-
signal: signal
|
|
446
|
+
signal: kiroRefreshSignal(signal),
|
|
145
447
|
});
|
|
146
|
-
if (!res.ok) throw
|
|
448
|
+
if (!res.ok) throw await kiroTokenRefreshError(res);
|
|
147
449
|
return readTokenResponse(res, refresh);
|
|
148
450
|
}
|
|
149
451
|
|
|
150
|
-
async function refreshAwsSsoOidcToken(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
452
|
+
async function refreshAwsSsoOidcToken(
|
|
453
|
+
refresh: string,
|
|
454
|
+
signal?: AbortSignal,
|
|
455
|
+
credential?: OAuthCredentials,
|
|
456
|
+
): Promise<OAuthCredentials> {
|
|
457
|
+
let metadata = credential?.kiro;
|
|
458
|
+
if (!metadata) {
|
|
459
|
+
const local = readImportedKiroCredential();
|
|
460
|
+
// Only a local store holding this exact refresh token describes this account. Anything else
|
|
461
|
+
// belongs to whichever account kiro-cli is currently signed into.
|
|
462
|
+
if (local?.refresh === refresh) metadata = metadataFromImported(local);
|
|
463
|
+
}
|
|
464
|
+
// A stored OCX account with no usable `kiro` metadata must still refresh account-scoped: falling
|
|
465
|
+
// through to `resolveKiroRegion(undefined)` would read KIRO_REGION or the local CLI import and
|
|
466
|
+
// borrow an unrelated account's region after a switch. Environment/manual credentials may still
|
|
467
|
+
// honor explicit KIRO_* routing; other stored accounts pin an empty marker (default region).
|
|
468
|
+
// Only a truly accountless refresh (no stored credential) keeps the legacy env/local fallback.
|
|
469
|
+
if (!metadata && credential) {
|
|
470
|
+
metadata = credential.source === "environment" || credential.source === "manual"
|
|
471
|
+
? environmentKiroRoutingMetadata() ?? {}
|
|
472
|
+
: {};
|
|
473
|
+
}
|
|
474
|
+
const clientId = metadata?.clientId;
|
|
475
|
+
const clientSecret = metadata?.clientSecret;
|
|
476
|
+
if (!clientId || !clientSecret) return refreshKiroDesktopToken(refresh, signal, metadata);
|
|
477
|
+
const region = resolveKiroRegion(metadata);
|
|
154
478
|
const run = async (refreshToken: string): Promise<Response> => fetch(OIDC_URL.replace("{region}", region), {
|
|
155
479
|
method: "POST",
|
|
156
480
|
headers: { "Content-Type": "application/json" },
|
|
157
481
|
body: JSON.stringify({
|
|
158
482
|
grantType: "refresh_token",
|
|
159
|
-
clientId
|
|
160
|
-
clientSecret
|
|
483
|
+
clientId,
|
|
484
|
+
clientSecret,
|
|
161
485
|
refreshToken,
|
|
162
486
|
}),
|
|
163
|
-
signal: signal
|
|
487
|
+
signal: kiroRefreshSignal(signal),
|
|
164
488
|
});
|
|
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}`);
|
|
489
|
+
const res = await run(refresh);
|
|
490
|
+
if (!res.ok) throw await kiroTokenRefreshError(res);
|
|
171
491
|
return readTokenResponse(res, refresh);
|
|
172
492
|
}
|
|
173
493
|
|
|
174
|
-
|
|
494
|
+
function matchingRotatedKiroCliCredential(
|
|
495
|
+
refresh: string,
|
|
496
|
+
credential?: OAuthCredentials,
|
|
497
|
+
): ImportedKiroCredential | undefined {
|
|
498
|
+
const storedIdentities = [credential?.kiro?.profileArn, credential?.accountId]
|
|
499
|
+
.filter((value): value is string => Boolean(value));
|
|
500
|
+
if (storedIdentities.length === 0 || new Set(storedIdentities).size !== 1) return undefined;
|
|
501
|
+
try {
|
|
502
|
+
const local = readKiroCliSqliteCredential();
|
|
503
|
+
if (!local?.profileArn || storedIdentities.some(identity => identity !== local.profileArn)) return undefined;
|
|
504
|
+
if (!local.refresh || local.refresh === refresh) return undefined;
|
|
505
|
+
return local;
|
|
506
|
+
} catch {
|
|
507
|
+
// An unrelated or malformed local store must not block the stored OCX account.
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function metadataForRotatedKiroCliCredential(
|
|
513
|
+
credential: OAuthCredentials,
|
|
514
|
+
local: ImportedKiroCredential,
|
|
515
|
+
): KiroOAuthMetadata {
|
|
516
|
+
const {
|
|
517
|
+
clientId: _storedClientId,
|
|
518
|
+
clientSecret: _storedClientSecret,
|
|
519
|
+
...storedRouting
|
|
520
|
+
} = credential.kiro ?? {};
|
|
521
|
+
const localMetadata = metadataFromImported(local) ?? {};
|
|
522
|
+
const {
|
|
523
|
+
clientId: _localClientId,
|
|
524
|
+
clientSecret: _localClientSecret,
|
|
525
|
+
...localRouting
|
|
526
|
+
} = localMetadata;
|
|
527
|
+
return {
|
|
528
|
+
...storedRouting,
|
|
529
|
+
...localRouting,
|
|
530
|
+
...(local.authType === "aws_sso_oidc" && local.clientId && local.clientSecret
|
|
531
|
+
? { clientId: local.clientId, clientSecret: local.clientSecret }
|
|
532
|
+
: {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function refreshKiroToken(
|
|
537
|
+
refresh: string,
|
|
538
|
+
signal?: AbortSignal,
|
|
539
|
+
credential?: OAuthCredentials,
|
|
540
|
+
): Promise<OAuthCredentials> {
|
|
175
541
|
if (!refresh) throw new Error("Kiro: no refresh token available (re-run `kiro-cli login`).");
|
|
176
|
-
|
|
542
|
+
try {
|
|
543
|
+
return await refreshAwsSsoOidcToken(refresh, signal, credential);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
if (!(error instanceof KiroTokenRefreshError) || error.httpStatus !== 400) throw error;
|
|
546
|
+
if (!credential) throw error;
|
|
547
|
+
const local = matchingRotatedKiroCliCredential(refresh, credential);
|
|
548
|
+
if (!local) throw error;
|
|
549
|
+
const retryMetadata = metadataForRotatedKiroCliCredential(credential, local);
|
|
550
|
+
const fresh = await refreshAwsSsoOidcToken(local.refresh, signal, {
|
|
551
|
+
...credential,
|
|
552
|
+
refresh: local.refresh,
|
|
553
|
+
kiro: retryMetadata,
|
|
554
|
+
});
|
|
555
|
+
return { ...fresh, kiro: retryMetadata };
|
|
556
|
+
}
|
|
177
557
|
}
|
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 });
|
|
@@ -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);
|