@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/index.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import type { OAuthController, OAuthCredentials } from "./types";
|
|
1
|
+
import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
|
|
2
2
|
import { parseCallbackInput } from "./callback-server";
|
|
3
3
|
import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
|
|
4
4
|
import { loadConfig, resolveEnvValue, saveConfig } from "../config";
|
|
5
5
|
import { maskEmail } from "../lib/privacy";
|
|
6
|
-
import {
|
|
6
|
+
import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
|
|
7
|
+
import { getAccountCredential, getAccountSet, removeAccount, saveAccountCredential, saveCredential, setActiveAccount, getCredential, credentialGeneration, createOAuthRefreshIntentLock, mergeAccountCredential, markAccountNeedsReauthIfGeneration, readOAuthRefreshIntent, writeOAuthRefreshIntent, clearOAuthRefreshIntent } from "./store";
|
|
7
8
|
import { loginXai, refreshXaiToken, XAI_LOCAL_CLI_DETACH_WARNING, XaiTokenRequestError } from "./xai";
|
|
8
9
|
import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
9
10
|
import { loginKimi, refreshKimiToken } from "./kimi";
|
|
10
|
-
import { loginKiro, readKiroCliSqlite, refreshKiroToken } from "./kiro";
|
|
11
11
|
import { loginChatGPT, refreshChatGPTToken } from "./chatgpt";
|
|
12
12
|
import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
|
|
13
13
|
import { loginCursor, refreshCursorToken } from "./cursor";
|
|
14
14
|
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
|
|
15
15
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
16
|
-
import {
|
|
16
|
+
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
17
|
+
import { effectiveGoogleMode, getProviderRegistryEntry } from "../providers/registry";
|
|
17
18
|
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
18
19
|
import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
19
20
|
import { logOAuthEvent } from "./log";
|
|
@@ -37,6 +38,7 @@ export {
|
|
|
37
38
|
type OAuthHealthLabel,
|
|
38
39
|
} from "./health";
|
|
39
40
|
export { OAUTH_REFRESH_LOCK_WAIT_MS, peekAuthStore, peekOAuthRefreshIntent } from "./store";
|
|
41
|
+
import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
|
|
40
42
|
|
|
41
43
|
const REFRESH_SKEW_MS = 60_000;
|
|
42
44
|
export interface OAuthAccessSnapshot {
|
|
@@ -44,6 +46,8 @@ export interface OAuthAccessSnapshot {
|
|
|
44
46
|
accountId: string;
|
|
45
47
|
generation: string;
|
|
46
48
|
accessToken: string;
|
|
49
|
+
/** Safe request-routing subset; refresh-only Kiro client secrets never leave the credential store. */
|
|
50
|
+
kiro?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
const tokenRefreshes = new Map<string, Promise<OAuthAccessSnapshot>>();
|
|
@@ -57,9 +61,18 @@ function cached(p:string,a:string,c:OAuthCredentials,now:()=>number){const k=ver
|
|
|
57
61
|
|
|
58
62
|
export interface LoginOpts { forceLogin?: boolean; /** When set, persist into this account slot and require matching identity. */ reauthAccountId?: string }
|
|
59
63
|
|
|
64
|
+
export interface LoginFlowLifecycle {
|
|
65
|
+
/** Runs after background credential/config persistence settles, before status becomes done. */
|
|
66
|
+
onSettled?: () => void | Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
interface OAuthProviderDef {
|
|
61
70
|
login(ctrl: OAuthController, opts?: LoginOpts): Promise<OAuthCredentials>;
|
|
62
|
-
refresh(
|
|
71
|
+
refresh(
|
|
72
|
+
refreshToken: string,
|
|
73
|
+
signal?: AbortSignal,
|
|
74
|
+
credential?: OAuthCredentials,
|
|
75
|
+
): Promise<OAuthCredentials>;
|
|
63
76
|
/** provider entry written into config.json on first login. */
|
|
64
77
|
providerConfig: OcxProviderConfig;
|
|
65
78
|
defaultModel: string;
|
|
@@ -107,8 +120,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
107
120
|
defaultModel: oauthDefaultModel("kimi"),
|
|
108
121
|
},
|
|
109
122
|
kiro: {
|
|
110
|
-
login: (ctrl) => loginKiro(ctrl),
|
|
111
|
-
refresh: (rt, signal) => refreshKiroToken(rt, signal),
|
|
123
|
+
login: (ctrl, opts) => loginKiro(ctrl, { forceLogin: opts?.forceLogin }),
|
|
124
|
+
refresh: (rt, signal, credential) => refreshKiroToken(rt, signal, credential),
|
|
112
125
|
providerConfig: oauthConfig("kiro"),
|
|
113
126
|
defaultModel: oauthDefaultModel("kiro"),
|
|
114
127
|
},
|
|
@@ -194,11 +207,25 @@ export class OAuthLoginRequiredError extends Error {
|
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
function accessSnapshot(provider: string, accountId: string, cred: OAuthCredentials): OAuthAccessSnapshot {
|
|
210
|
+
const storedKiroRouting = {
|
|
211
|
+
...(cred.kiro?.profileArn ? { profileArn: cred.kiro.profileArn } : {}),
|
|
212
|
+
...(cred.kiro?.apiRegion ? { apiRegion: cred.kiro.apiRegion } : {}),
|
|
213
|
+
...(cred.kiro?.ssoRegion ? { ssoRegion: cred.kiro.ssoRegion } : {}),
|
|
214
|
+
};
|
|
197
215
|
return {
|
|
198
216
|
provider,
|
|
199
217
|
accountId,
|
|
200
218
|
generation: credentialGeneration(cred),
|
|
201
219
|
accessToken: cred.access,
|
|
220
|
+
// Stored account metadata remains authoritative. Metadata-less legacy/environment credentials
|
|
221
|
+
// may use explicit environment routing, but never borrow the currently signed-in local CLI account.
|
|
222
|
+
...(provider === "kiro"
|
|
223
|
+
? {
|
|
224
|
+
kiro: Object.keys(storedKiroRouting).length > 0
|
|
225
|
+
? storedKiroRouting
|
|
226
|
+
: environmentKiroRoutingMetadata() ?? {},
|
|
227
|
+
}
|
|
228
|
+
: {}),
|
|
202
229
|
};
|
|
203
230
|
}
|
|
204
231
|
|
|
@@ -267,12 +294,6 @@ export async function getValidAccessTokenForAccount(provider: string, accountId:
|
|
|
267
294
|
return (await resolveAccessSnapshotForAccount(provider, accountId)).accessToken;
|
|
268
295
|
}
|
|
269
296
|
|
|
270
|
-
function readFreshKiroCliCredential(): OAuthCredentials | undefined {
|
|
271
|
-
const imported = readKiroCliSqlite();
|
|
272
|
-
if (!imported || imported.expires <= Date.now() + REFRESH_SKEW_MS) return undefined;
|
|
273
|
-
return { access: imported.access, refresh: imported.refresh, expires: imported.expires, source: "local-cli" };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
297
|
/** Terminal refresh failures (revoked/rotated-away grants) — retrying cannot succeed. */
|
|
277
298
|
function isTerminalRefreshError(err: unknown): boolean {
|
|
278
299
|
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
@@ -286,6 +307,7 @@ function isTerminalRefreshError(err: unknown): boolean {
|
|
|
286
307
|
function terminal(error:unknown):boolean{
|
|
287
308
|
if(error instanceof XaiTokenRequestError)return ["invalid_grant","refresh_token_reused","revoked_token"].includes(error.oauthError??"");
|
|
288
309
|
if(error instanceof AnthropicTokenError)return (error.httpStatus===400||error.httpStatus===401)&&["invalid_grant","refresh_token_reused","revoked","revoked_token","refresh_token_revoked"].includes(error.oauthError??"");
|
|
310
|
+
if(error instanceof KiroTokenRefreshError)return (error.httpStatus===400||error.httpStatus===401)&&error.oauthError!==undefined;
|
|
289
311
|
return isTerminalRefreshError(error);
|
|
290
312
|
}
|
|
291
313
|
function authoritative(stored:OAuthCredentials,active:boolean,now:()=>number):OAuthCredentials{if(stored.source!=="local-cli")return stored;const disk=detectGrokCliToken();if(!disk)return stored;const allowed=isSameGrokIdentity(stored,disk)||(active&&!hasComparableGrokIdentity(stored,disk));return allowed&&shouldAdoptGrokGeneration(stored,disk,now(),REFRESH_SKEW_MS)?disk:stored;}
|
|
@@ -297,6 +319,7 @@ function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCrede
|
|
|
297
319
|
...(fresh.apiBaseUrl === undefined && previous.apiBaseUrl ? { apiBaseUrl: previous.apiBaseUrl } : {}),
|
|
298
320
|
...(fresh.email === undefined && previous.email ? { email: previous.email } : {}),
|
|
299
321
|
...(fresh.accountId === undefined && previous.accountId ? { accountId: previous.accountId } : {}),
|
|
322
|
+
...(fresh.kiro === undefined && previous.kiro ? { kiro: previous.kiro } : {}),
|
|
300
323
|
};
|
|
301
324
|
}
|
|
302
325
|
export async function refreshXaiAccountWithLock(provider:string,accountId:string,def:OAuthProviderDef,callerCredential:OAuthCredentials,deps:XaiRefreshDeps={}):Promise<string>{const now=deps.now??Date.now;const guard=await(deps.intentLock??createOAuthRefreshIntentLock(provider,accountId)).acquire();try{const stored=getAccountCredential(provider,accountId);if(!stored)throw new OAuthLoginRequiredError(provider);const active=getAccountSet(provider)?.activeAccountId===accountId,candidate=authoritative(stored,active,now);if(credentialGeneration(candidate)!==credentialGeneration(callerCredential)&&candidate.expires>now()+REFRESH_SKEW_MS){if(credentialGeneration(candidate)!==credentialGeneration(stored)){const o=await mergeAccountCredential(provider,accountId,candidate,{expectedGeneration:credentialGeneration(stored),afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}}return candidate.access;}if(cached(provider,accountId,candidate,now))throw new OAuthLoginRequiredError(provider);const generation=credentialGeneration(candidate);try{const fresh=merged(await def.refresh(candidate.refresh),candidate);const o=await mergeAccountCredential(provider,accountId,fresh,{expectedGeneration:generation,afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));if(candidate.source==="local-cli")console.warn(XAI_LOCAL_CLI_DETACH_WARNING);return fresh.access;}catch(error){if(!terminal(error))throw error;permanentRefreshFailures.set(verdictKey(provider,accountId,candidate),now()+XAI_PERMANENT_FAILURE_TTL_MS);await markAccountNeedsReauthIfGeneration(provider,accountId,generation);throw new OAuthLoginRequiredError(provider);}}finally{guard.release();}}
|
|
@@ -395,7 +418,7 @@ export async function refreshGenericAccountWithLock(
|
|
|
395
418
|
}
|
|
396
419
|
const generation = credentialGeneration(stored);
|
|
397
420
|
try {
|
|
398
|
-
const fresh = merged(await def.refresh(stored.refresh), stored);
|
|
421
|
+
const fresh = merged(await def.refresh(stored.refresh, undefined, stored), stored);
|
|
399
422
|
const outcome = await mergeAccountCredential(provider, accountId, fresh, {
|
|
400
423
|
expectedGeneration: generation,
|
|
401
424
|
afterPrePersistRead: deps.afterPrePersistRead,
|
|
@@ -407,7 +430,7 @@ export async function refreshGenericAccountWithLock(
|
|
|
407
430
|
logOAuthEvent("OAuth credentials rotated and persisted", { provider, accountId });
|
|
408
431
|
return fresh.access;
|
|
409
432
|
} catch (error) {
|
|
410
|
-
if (!
|
|
433
|
+
if (!terminal(error)) throw error;
|
|
411
434
|
await markAccountNeedsReauthIfGeneration(provider, accountId, generation);
|
|
412
435
|
throw new OAuthLoginRequiredError(provider);
|
|
413
436
|
}
|
|
@@ -422,30 +445,9 @@ async function refreshAndPersistAccessToken(
|
|
|
422
445
|
def: OAuthProviderDef,
|
|
423
446
|
cred: OAuthCredentials,
|
|
424
447
|
): Promise<string> {
|
|
425
|
-
// Local-CLI import fallback only for the ACTIVE account: importing another identity's
|
|
426
|
-
// token under a background account id would silently contaminate that account.
|
|
427
|
-
const isActive = getAccountSet(provider)?.activeAccountId === accountId;
|
|
428
|
-
if (provider === "kiro" && isActive) {
|
|
429
|
-
const imported = readFreshKiroCliCredential();
|
|
430
|
-
if (imported) {
|
|
431
|
-
await saveCredential(provider, imported);
|
|
432
|
-
return imported.access;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
448
|
if (provider === "xai") return refreshXaiAccountWithLock(provider, accountId, def, cred);
|
|
436
449
|
if (provider === "anthropic") return refreshAnthropicAccountWithLock(provider, accountId, def, cred);
|
|
437
|
-
|
|
438
|
-
return await refreshGenericAccountWithLock(provider, accountId, def, cred);
|
|
439
|
-
} catch (err) {
|
|
440
|
-
if (provider === "kiro" && isActive) {
|
|
441
|
-
const imported = readFreshKiroCliCredential();
|
|
442
|
-
if (imported) {
|
|
443
|
-
await saveCredential(provider, imported);
|
|
444
|
-
return imported.access;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
throw err;
|
|
448
|
-
}
|
|
450
|
+
return refreshGenericAccountWithLock(provider, accountId, def, cred);
|
|
449
451
|
}
|
|
450
452
|
|
|
451
453
|
/**
|
|
@@ -468,8 +470,9 @@ export async function resolveModelsAuthToken(name: string, prov: OcxProviderConf
|
|
|
468
470
|
/**
|
|
469
471
|
* Provider-correct `GET /models` request (URL + headers), so both model-listing paths fetch the
|
|
470
472
|
* LIVE catalog correctly per adapter. Anthropic is the special case: its endpoint is `/v1/models`
|
|
471
|
-
* (not `/models`), it needs `anthropic-version`, and it authenticates with `x-api-key`
|
|
472
|
-
* `Authorization: Bearer`
|
|
473
|
+
* (not `/models`), it needs `anthropic-version`, and it authenticates with `x-api-key` by default
|
|
474
|
+
* (or `Authorization: Bearer` when `apiKeyTransport = "bearer"`), plus the OAuth beta for oauth
|
|
475
|
+
* mode — not a bare Bearer. Google (ai-studio mode)
|
|
473
476
|
* is the other special case: `x-goog-api-key` + `/v1beta/models`, returning `{ models: [...] }`.
|
|
474
477
|
* The catalog authority gate intentionally degrades that non-OpenAI shape to stale/static data.
|
|
475
478
|
* Everyone else uses the OpenAI-style `/models` + Bearer with a `{ data: [{ id, owned_by? }] }`
|
|
@@ -492,14 +495,16 @@ export function buildModelsRequest(prov: OcxProviderConfig, apiKey: string | und
|
|
|
492
495
|
return { url: `${effectiveProvider.baseUrl}/v1beta/models?pageSize=1000`, headers };
|
|
493
496
|
}
|
|
494
497
|
if (effectiveProvider.adapter === "anthropic") {
|
|
498
|
+
const base = effectiveProvider.baseUrl.replace(/\/v1\/?$/, "");
|
|
495
499
|
headers["anthropic-version"] = "2023-06-01";
|
|
496
500
|
if (effectiveProvider.authMode === "oauth") {
|
|
497
501
|
headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
|
|
498
502
|
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
499
503
|
} else if (apiKey) {
|
|
500
|
-
headers["
|
|
504
|
+
if (effectiveProvider.apiKeyTransport === "bearer") headers["Authorization"] = `Bearer ${apiKey}`;
|
|
505
|
+
else headers["x-api-key"] = apiKey;
|
|
501
506
|
}
|
|
502
|
-
return { url: `${
|
|
507
|
+
return { url: `${base}/v1/models?limit=1000`, headers };
|
|
503
508
|
}
|
|
504
509
|
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
505
510
|
return { url: `${effectiveProvider.baseUrl}/models`, headers };
|
|
@@ -566,39 +571,219 @@ export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
|
566
571
|
return changed;
|
|
567
572
|
}
|
|
568
573
|
|
|
569
|
-
/**
|
|
574
|
+
/** Runtime guards: provider config is intentionally passthrough, so persisted fields may be malformed. */
|
|
575
|
+
function preservableApiKeyPool(value: unknown): NonNullable<OcxProviderConfig["apiKeyPool"]> | undefined {
|
|
576
|
+
if (!Array.isArray(value)) return undefined;
|
|
577
|
+
const pool: NonNullable<OcxProviderConfig["apiKeyPool"]> = [];
|
|
578
|
+
const ids = new Set<string>();
|
|
579
|
+
const keys = new Set<string>();
|
|
580
|
+
for (const entry of value as unknown[]) {
|
|
581
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
582
|
+
const candidate = entry as Record<string, unknown>;
|
|
583
|
+
const id = typeof candidate.id === "string" ? candidate.id.trim() : "";
|
|
584
|
+
const key = sanitizeApiKeyValue(candidate.key);
|
|
585
|
+
if (!id || !key || ids.has(id) || keys.has(key)) continue;
|
|
586
|
+
const label = typeof candidate.label === "string" ? candidate.label : undefined;
|
|
587
|
+
const addedAt = typeof candidate.addedAt === "number" && Number.isFinite(candidate.addedAt)
|
|
588
|
+
? candidate.addedAt
|
|
589
|
+
: undefined;
|
|
590
|
+
ids.add(id);
|
|
591
|
+
keys.add(key);
|
|
592
|
+
pool.push({
|
|
593
|
+
id,
|
|
594
|
+
key,
|
|
595
|
+
...(label !== undefined ? { label } : {}),
|
|
596
|
+
...(addedAt !== undefined ? { addedAt } : {}),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
// `apiKey` remains the routing source of truth. Keep valid alternate slots even when a
|
|
600
|
+
// hand-edited config left the pool out of sync, rather than deleting usable credentials.
|
|
601
|
+
return pool.length > 0 ? pool : undefined;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Add/refresh an OAuth provider's config entry on a config object (does not persist).
|
|
606
|
+
*
|
|
607
|
+
* Providers whose registry entry sets `allowKeyAuthOverride` (xai, github-copilot) can be
|
|
608
|
+
* billed through a stored API key instead of the OAuth login (router.ts honors
|
|
609
|
+
* `authMode: "key"` for them). A blind preset overwrite here deletes `apiKey`/`apiKeyPool`
|
|
610
|
+
* on every OAuth login, silently destroying the stored key and forcing a re-paste — and it
|
|
611
|
+
* flips billing back to the subscription without the user asking. Carry the key fields over
|
|
612
|
+
* and keep key billing while usable key material remains and the user was not explicitly on
|
|
613
|
+
* oauth. If the final key was removed and only the old key mode remains, let the OAuth
|
|
614
|
+
* preset restore `authMode: "oauth"` so the newly saved OAuth credential can be used.
|
|
615
|
+
*
|
|
616
|
+
* After preservation, `apiKey` always has exactly one matching pool entry (inserting via the
|
|
617
|
+
* same content-derived id as the API-key manager when the active key was missing from the
|
|
618
|
+
* pool). Key mode reflects stored user intent (explicit `"key"` or omitted mode with safe
|
|
619
|
+
* key material) — never whether the login CLI process can resolve an env reference. Env-backed
|
|
620
|
+
* availability is decided at proxy routing time in `router.ts`.
|
|
621
|
+
*/
|
|
570
622
|
export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
571
623
|
if (provider === "chatgpt") return;
|
|
572
624
|
const def = OAUTH_PROVIDERS[provider];
|
|
573
625
|
if (!def) return;
|
|
574
|
-
|
|
626
|
+
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
|
|
627
|
+
if (namespaceCollision) throw new Error(namespaceCollision);
|
|
628
|
+
const existing = config.providers[provider];
|
|
629
|
+
const next: OcxProviderConfig = { ...def.providerConfig };
|
|
630
|
+
if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
|
|
631
|
+
// Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
|
|
632
|
+
let storedApiKey = sanitizeApiKeyValue(existing.apiKey);
|
|
633
|
+
const storedApiKeyPool = preservableApiKeyPool(existing.apiKeyPool);
|
|
634
|
+
// Unsafe/blank active key with a usable pool: promote the first safe pool entry so
|
|
635
|
+
// key billing keeps working instead of falling back to oauth while pool keys remain.
|
|
636
|
+
if (storedApiKey === undefined && storedApiKeyPool && storedApiKeyPool.length > 0) {
|
|
637
|
+
storedApiKey = storedApiKeyPool[0]!.key;
|
|
638
|
+
}
|
|
639
|
+
if (storedApiKey !== undefined) {
|
|
640
|
+
const pool = storedApiKeyPool ? [...storedApiKeyPool] : [];
|
|
641
|
+
// Keep routing and listProviderApiKeys in sync: never leave a hidden active key that
|
|
642
|
+
// is absent from the pool (listing would fall back to pool[0] as "active").
|
|
643
|
+
if (!pool.some(entry => entry.key === storedApiKey)) {
|
|
644
|
+
pool.push({ id: apiKeyPoolEntryId(storedApiKey), key: storedApiKey });
|
|
645
|
+
}
|
|
646
|
+
next.apiKey = storedApiKey;
|
|
647
|
+
next.apiKeyPool = pool;
|
|
648
|
+
const previousModeAllowsKey = existing.authMode === "key" || existing.authMode === undefined;
|
|
649
|
+
if (previousModeAllowsKey) next.authMode = "key";
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
config.providers[provider] = next;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
interface RunLoginDeps {
|
|
656
|
+
saveCredential?: typeof saveCredential;
|
|
657
|
+
saveAccountCredential?: typeof saveAccountCredential;
|
|
658
|
+
loadConfig?: typeof loadConfig;
|
|
659
|
+
saveConfig?: typeof saveConfig;
|
|
660
|
+
settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
|
|
661
|
+
removeAccount?: typeof removeAccount;
|
|
662
|
+
setActiveAccount?: typeof setActiveAccount;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Roll back only accounts created by this forced login, preserving concurrent refreshes of others. */
|
|
666
|
+
async function rollbackForcedKiroAccountWrite(
|
|
667
|
+
provider: string,
|
|
668
|
+
previousActiveId: string | undefined,
|
|
669
|
+
previousAccountIds: ReadonlySet<string>,
|
|
670
|
+
deps: Pick<RunLoginDeps, "removeAccount" | "setActiveAccount">,
|
|
671
|
+
): Promise<void> {
|
|
672
|
+
const set = getAccountSet(provider);
|
|
673
|
+
if (!set) return;
|
|
674
|
+
for (const account of [...set.accounts]) {
|
|
675
|
+
if (previousAccountIds.has(account.id)) continue;
|
|
676
|
+
await (deps.removeAccount ?? removeAccount)(provider, account.id);
|
|
677
|
+
}
|
|
678
|
+
if (previousActiveId && getAccountCredential(provider, previousActiveId)) {
|
|
679
|
+
await (deps.setActiveAccount ?? setActiveAccount)(provider, previousActiveId);
|
|
680
|
+
}
|
|
575
681
|
}
|
|
576
682
|
|
|
577
683
|
/** Run the login flow, persist the credential + upsert the provider entry to disk, return cred. */
|
|
578
|
-
export async function runLogin(
|
|
684
|
+
export async function runLogin(
|
|
685
|
+
provider: string,
|
|
686
|
+
ctrl: OAuthController,
|
|
687
|
+
opts?: LoginOpts,
|
|
688
|
+
deps: RunLoginDeps = {},
|
|
689
|
+
): Promise<OAuthCredentials> {
|
|
579
690
|
const def = OAUTH_PROVIDERS[provider];
|
|
580
691
|
if (!def) throw new UnsupportedOAuthProviderError(provider);
|
|
692
|
+
const loadLatestConfig = deps.loadConfig ?? loadConfig;
|
|
693
|
+
const saveLatestConfig = deps.saveConfig ?? saveConfig;
|
|
694
|
+
if (provider !== "chatgpt") {
|
|
695
|
+
const preflightConfig = loadLatestConfig();
|
|
696
|
+
const namespaceCollision = codexAccountNamespaceProviderCollisionError(
|
|
697
|
+
preflightConfig.codexAccountNamespaces,
|
|
698
|
+
provider,
|
|
699
|
+
);
|
|
700
|
+
if (namespaceCollision) throw new Error(namespaceCollision);
|
|
701
|
+
}
|
|
702
|
+
// loginKiro keys its pending CLI-session transaction by object identity. Keep this exact object
|
|
703
|
+
// for settlement even when source normalization below creates a derived credential object.
|
|
704
|
+
const shouldRollbackKiroAccounts = provider === "kiro" && opts?.forceLogin === true;
|
|
705
|
+
const previousKiroAccounts = shouldRollbackKiroAccounts ? getAccountSet(provider) : undefined;
|
|
706
|
+
const previousKiroActiveId = previousKiroAccounts?.activeAccountId;
|
|
707
|
+
const previousKiroAccountIds = new Set(previousKiroAccounts?.accounts.map(account => account.id) ?? []);
|
|
581
708
|
const rawCred = await def.login(ctrl, opts);
|
|
582
709
|
const cred: OAuthCredentials = rawCred.source ? rawCred : { ...rawCred, source: "oauth" };
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
710
|
+
const settleKiroTransaction = deps.settleKiroLoginTransaction ?? settleKiroLoginTransaction;
|
|
711
|
+
try {
|
|
712
|
+
// Validate the provider row before credential persistence. A namespace claimed during the
|
|
713
|
+
// credential write is handled again below before the latest row is re-upserted.
|
|
714
|
+
if (provider !== "chatgpt") {
|
|
715
|
+
const preCommitConfig = loadLatestConfig();
|
|
716
|
+
upsertOAuthProvider(preCommitConfig, provider);
|
|
590
717
|
}
|
|
591
|
-
if (
|
|
592
|
-
|
|
718
|
+
if (opts?.reauthAccountId) {
|
|
719
|
+
const existing = getAccountCredential(provider, opts.reauthAccountId);
|
|
720
|
+
if (!existing) throw new Error(`Unknown account for reauth: ${opts.reauthAccountId}`);
|
|
721
|
+
if (!existing.accountId && !existing.email) {
|
|
722
|
+
throw new Error("Could not verify signed-in account identity for reauth.");
|
|
723
|
+
}
|
|
724
|
+
const identityMatches = existing.accountId && cred.accountId
|
|
725
|
+
? existing.accountId === cred.accountId
|
|
726
|
+
: existing.email && cred.email
|
|
727
|
+
? existing.email.toLowerCase() === cred.email.toLowerCase()
|
|
728
|
+
: false;
|
|
729
|
+
if (!identityMatches) {
|
|
730
|
+
throw new Error("Signed-in account does not match the selected account. Sign in with the same account.");
|
|
731
|
+
}
|
|
732
|
+
await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred);
|
|
733
|
+
} else {
|
|
734
|
+
await (deps.saveCredential ?? saveCredential)(provider, cred, {
|
|
735
|
+
preserveIdentityless: provider === "kiro" && opts?.forceLogin === true,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (provider !== "chatgpt") {
|
|
739
|
+
// Re-run against post-credential state so same-provider API-key additions, removals,
|
|
740
|
+
// and active-key switches survive. A late namespace claim wins over provider creation.
|
|
741
|
+
const latestConfig = loadLatestConfig();
|
|
742
|
+
const lateCollision = codexAccountNamespaceProviderCollisionError(
|
|
743
|
+
latestConfig.codexAccountNamespaces,
|
|
744
|
+
provider,
|
|
745
|
+
);
|
|
746
|
+
if (lateCollision) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
`${lateCollision}. The credential for "${provider}" was saved, but the provider entry was not written. `
|
|
749
|
+
+ "Rename the account selector, then re-run the login.",
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
upsertOAuthProvider(latestConfig, provider);
|
|
753
|
+
saveLatestConfig(latestConfig);
|
|
754
|
+
}
|
|
755
|
+
} catch (error) {
|
|
756
|
+
const errors: unknown[] = [error];
|
|
757
|
+
if (shouldRollbackKiroAccounts) {
|
|
758
|
+
try {
|
|
759
|
+
await rollbackForcedKiroAccountWrite(provider, previousKiroActiveId, previousKiroAccountIds, deps);
|
|
760
|
+
} catch (rollbackError) {
|
|
761
|
+
errors.push(rollbackError);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
settleKiroTransaction(rawCred, false);
|
|
766
|
+
} catch (restoreError) {
|
|
767
|
+
errors.push(restoreError);
|
|
768
|
+
}
|
|
769
|
+
if (errors.length > 1) {
|
|
770
|
+
throw new AggregateError(
|
|
771
|
+
errors,
|
|
772
|
+
"Kiro login persistence failed and the previous Kiro CLI session could not be restored.",
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
settleKiroTransaction(rawCred, true);
|
|
778
|
+
if (provider !== "chatgpt") {
|
|
779
|
+
try {
|
|
780
|
+
const { clearAccountQuotaCache, clearProviderQuotaCache } = await import("../providers/quota");
|
|
781
|
+
clearProviderQuotaCache();
|
|
782
|
+
clearAccountQuotaCache(provider);
|
|
783
|
+
} catch {
|
|
784
|
+
// Quota module may be unavailable in tightly scoped unit tests.
|
|
593
785
|
}
|
|
594
|
-
await saveAccountCredential(provider, opts.reauthAccountId, cred);
|
|
595
|
-
} else {
|
|
596
|
-
await saveCredential(provider, cred);
|
|
597
786
|
}
|
|
598
|
-
if (provider === "chatgpt") return cred;
|
|
599
|
-
const config = loadConfig();
|
|
600
|
-
upsertOAuthProvider(config, provider);
|
|
601
|
-
saveConfig(config);
|
|
602
787
|
return cred;
|
|
603
788
|
}
|
|
604
789
|
|
|
@@ -745,7 +930,11 @@ export function cancelLoginFlow(provider: string): boolean {
|
|
|
745
930
|
return true;
|
|
746
931
|
}
|
|
747
932
|
|
|
748
|
-
export async function startLoginFlow(
|
|
933
|
+
export async function startLoginFlow(
|
|
934
|
+
provider: string,
|
|
935
|
+
opts?: LoginOpts,
|
|
936
|
+
lifecycle?: LoginFlowLifecycle,
|
|
937
|
+
): Promise<{ url: string; instructions?: string; deviceCode?: string }> {
|
|
749
938
|
const def = OAUTH_PROVIDERS[provider];
|
|
750
939
|
if (!def) throw new UnsupportedOAuthProviderError(provider);
|
|
751
940
|
const existing = loginState.get(provider);
|
|
@@ -768,22 +957,44 @@ export async function startLoginFlow(provider: string, opts?: LoginOpts): Promis
|
|
|
768
957
|
onManualCodeInput: (expectedState?: string) => waitForManualLoginCode(provider, abort.signal, expectedState),
|
|
769
958
|
signal: abort.signal,
|
|
770
959
|
};
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
960
|
+
const settle = async (error?: unknown): Promise<void> => {
|
|
961
|
+
let finalError = error;
|
|
962
|
+
try {
|
|
963
|
+
await lifecycle?.onSettled?.();
|
|
964
|
+
} catch (settleError) {
|
|
965
|
+
// A successful credential/config commit is not fully live until its owner reconciles the
|
|
966
|
+
// runtime config. For an already-failed login, keep the original recovery error.
|
|
967
|
+
if (finalError === undefined) finalError = settleError;
|
|
968
|
+
}
|
|
969
|
+
if (finalError === undefined) {
|
|
774
970
|
loginAbort.delete(provider);
|
|
775
971
|
clearManualCodeSlot(provider);
|
|
776
972
|
loginState.set(provider, { done: true });
|
|
777
973
|
// Local-token import (grok-cli / Claude Code keychain) completes WITHOUT firing onAuth —
|
|
778
974
|
// resolve so the GUI call returns instead of hanging.
|
|
779
975
|
if (!urlResolved) resolve({ url: "", instructions: "Logged in via an existing local CLI/keychain token — no browser needed." });
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
});
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const e = finalError;
|
|
980
|
+
loginAbort.delete(provider);
|
|
981
|
+
clearManualCodeSlot(provider);
|
|
982
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
983
|
+
loginState.set(provider, { done: true, error: msg });
|
|
984
|
+
if (!urlResolved) reject(e);
|
|
985
|
+
};
|
|
986
|
+
// Background: runLogin persists the credential + provider entry to disk. The lifecycle hook
|
|
987
|
+
// lets a long-lived server config adopt that settled state before clients observe done=true.
|
|
988
|
+
void runLogin(provider, ctrl, opts).then(
|
|
989
|
+
() => settle(),
|
|
990
|
+
(e: unknown) => settle(e),
|
|
991
|
+
).catch((e: unknown) => {
|
|
992
|
+
// settle catches lifecycle failures, so this is only a defensive promise-boundary guard.
|
|
993
|
+
loginAbort.delete(provider);
|
|
994
|
+
clearManualCodeSlot(provider);
|
|
995
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
996
|
+
loginState.set(provider, { done: true, error: msg });
|
|
997
|
+
if (!urlResolved) reject(e);
|
|
998
|
+
});
|
|
788
999
|
});
|
|
789
1000
|
}
|
|
@@ -5,7 +5,8 @@ import { deriveKeyLoginMap, enrichProviderFromRegistry, type DerivedKeyLoginProv
|
|
|
5
5
|
* API-key "login" providers: not OAuth — the flow opens the provider's dashboard so the user can
|
|
6
6
|
* create/copy a key, then validates + stores it as the provider's `apiKey` (authMode "key").
|
|
7
7
|
* Most use the OpenAI-compatible chat API (`openai-chat` adapter, `Authorization: Bearer <key>`); a
|
|
8
|
-
* few expose only an Anthropic-compatible endpoint and set `adapter: "anthropic"` (
|
|
8
|
+
* few expose only an Anthropic-compatible endpoint and set `adapter: "anthropic"` (default
|
|
9
|
+
* `x-api-key`, optional bearer via `apiKeyTransport`).
|
|
9
10
|
*/
|
|
10
11
|
export interface KeyLoginProvider extends DerivedKeyLoginProvider {}
|
|
11
12
|
|
|
@@ -29,6 +30,20 @@ export function listKeyLoginProviders(): Array<{ id: string } & KeyLoginProvider
|
|
|
29
30
|
return Object.entries(KEY_LOGIN_PROVIDERS).map(([id, p]) => ({ id, ...p }));
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function anthropicKeyValidationHeaders(provider: Pick<KeyLoginProvider, "apiKeyTransport">, key: string): HeadersInit {
|
|
34
|
+
return provider.apiKeyTransport === "bearer"
|
|
35
|
+
? {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
"anthropic-version": "2023-06-01",
|
|
38
|
+
"Authorization": `Bearer ${key}`,
|
|
39
|
+
}
|
|
40
|
+
: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"anthropic-version": "2023-06-01",
|
|
43
|
+
"x-api-key": key,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/** Best-effort key validation. Returns true/false/unknown; never persists the key itself. */
|
|
33
48
|
export async function validateApiKey(provider: KeyLoginProvider, key: string): Promise<boolean | "unknown"> {
|
|
34
49
|
try {
|
|
@@ -36,11 +51,7 @@ export async function validateApiKey(provider: KeyLoginProvider, key: string): P
|
|
|
36
51
|
const base = provider.baseUrl.replace(/\/v1\/?$/, "");
|
|
37
52
|
const res = await fetch(`${base}/v1/messages`, {
|
|
38
53
|
method: "POST",
|
|
39
|
-
headers:
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
"anthropic-version": "2023-06-01",
|
|
42
|
-
"x-api-key": key,
|
|
43
|
-
},
|
|
54
|
+
headers: anthropicKeyValidationHeaders(provider, key),
|
|
44
55
|
body: JSON.stringify({
|
|
45
56
|
model: provider.defaultModel ?? "claude-haiku-4-5",
|
|
46
57
|
max_tokens: 1,
|
package/src/oauth/kimi.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
6
7
|
import { getConfigDir } from "../config";
|
|
7
8
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
8
9
|
|
|
@@ -106,6 +107,7 @@ function getDeviceId(): string {
|
|
|
106
107
|
if ((e as { code?: string })?.code !== "ENOENT") throw e;
|
|
107
108
|
}
|
|
108
109
|
const id = randomUUID().replace(/-/g, "");
|
|
110
|
+
recordOwnedConfigPath(getConfigDir(), p);
|
|
109
111
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
110
112
|
writeFileSync(p, id + "\n", { mode: 0o600 });
|
|
111
113
|
deviceIdCache = id;
|