@bitkyc08/opencodex 2.76.0 → 2.77.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/gui/dist/assets/App-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build sends its conversation identity as `x-grok-conv-id` (and the same value as the body
|
|
3
|
+
* `prompt_cache_key`), but never the `session_id` header Codex clients send. The ChatGPT Codex
|
|
4
|
+
* backend only reuses a warmed prompt prefix for requests that carry `session_id`, and opencodex
|
|
5
|
+
* reads the same header for thread affinity and conversation logging. Without it, every Grok turn
|
|
6
|
+
* routed to an OpenAI model was a cold `new_bind` with near-zero cached input.
|
|
7
|
+
*
|
|
8
|
+
* Only the managed Grok surface (`x-opencodex-grok: 1`, written by `ocx` into Grok's config) is
|
|
9
|
+
* promoted, and an explicit caller session header always wins.
|
|
10
|
+
*/
|
|
11
|
+
const GROK_SURFACE_HEADER = "x-opencodex-grok";
|
|
12
|
+
const GROK_CONVERSATION_HEADER = "x-grok-conv-id";
|
|
13
|
+
const SESSION_HEADERS = ["session_id", "session-id", "thread-id"] as const;
|
|
14
|
+
// Grok emits UUIDs and `turn-summary-<uuid>`; anything else is not promoted.
|
|
15
|
+
const SAFE_CONVERSATION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
16
|
+
|
|
17
|
+
export function grokConversationSessionId(headers: Headers): string | undefined {
|
|
18
|
+
if (headers.get(GROK_SURFACE_HEADER) !== "1") return undefined;
|
|
19
|
+
if (SESSION_HEADERS.some(name => headers.has(name))) return undefined;
|
|
20
|
+
const conversationId = headers.get(GROK_CONVERSATION_HEADER)?.trim();
|
|
21
|
+
if (!conversationId || !SAFE_CONVERSATION_ID.test(conversationId)) return undefined;
|
|
22
|
+
return conversationId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Returns `req` unchanged unless a Grok conversation id can stand in for the missing `session_id`. */
|
|
26
|
+
export function withGrokSessionIdentity(req: Request): Request {
|
|
27
|
+
const sessionId = grokConversationSessionId(req.headers);
|
|
28
|
+
if (!sessionId) return req;
|
|
29
|
+
const headers = new Headers(req.headers);
|
|
30
|
+
headers.set("session_id", sessionId);
|
|
31
|
+
// The clone keeps the unread body stream and the caller's abort signal.
|
|
32
|
+
return new Request(req, { headers });
|
|
33
|
+
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
/** Refresh only previously connected clients; a refused file never blocks its peers. */
|
|
12
12
|
export async function refreshOwnedCatalogIntegrations(
|
|
13
13
|
input: Omit<OwnedIntegrationRefreshInput, "clientId">,
|
|
14
|
-
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "droid"],
|
|
14
|
+
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "droid", "opencode", "kilo"],
|
|
15
15
|
): Promise<OwnedIntegrationRefreshOutcome[]> {
|
|
16
16
|
// Client files are shared with the live proxy a sibling instance runs beside; their entries
|
|
17
17
|
// point at the owner's port, and refreshing them here would re-point them at this one.
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from "./registry";
|
|
37
37
|
import { declaredIntegrationTarget, resolveIntegrationTarget, type IntegrationTarget } from "./target";
|
|
38
38
|
import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
39
|
-
import { classifyIntegration, exportContextOf, readPath, type IntegrationState, type StateReason } from "./state";
|
|
39
|
+
import { buildIntegrationContribution, classifyIntegration, exportContextOf, readPath, type IntegrationState, type StateReason } from "./state";
|
|
40
40
|
import { inspectKiloCandidates } from "./kilo-candidates";
|
|
41
41
|
import { InvalidSelectorError } from "./merge";
|
|
42
42
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
@@ -791,6 +791,8 @@ export interface IntegrationWriteInput {
|
|
|
791
791
|
models: readonly ExportModel[];
|
|
792
792
|
config: OcxConfig;
|
|
793
793
|
port: number;
|
|
794
|
+
/** Explicit Droid-only defaults; omitted means retain matching owned rows. */
|
|
795
|
+
droidReasoningDefaults?: Record<string, string>;
|
|
794
796
|
env?: NodeJS.ProcessEnv;
|
|
795
797
|
home?: string;
|
|
796
798
|
store?: IntegrationStateStore;
|
|
@@ -936,13 +938,13 @@ export function observeIntegration(
|
|
|
936
938
|
* reads a different document there, and a write in the config file's shape
|
|
937
939
|
* would be as unread as a write to the config file itself.
|
|
938
940
|
*/
|
|
939
|
-
const contribution = effective.buildContribution(exportContextOf(input));
|
|
940
941
|
// A record proves ownership of the file it was written FOR. Matching only by
|
|
941
942
|
// client id let a record for one home authorize a write to another whose
|
|
942
943
|
// bytes happened to hash the same — which deleted a config we never touched.
|
|
943
944
|
const record = stored && stored.clientId === clientId && stored.configPath === configPath
|
|
944
945
|
? stored
|
|
945
946
|
: null;
|
|
947
|
+
const contribution = buildIntegrationContribution(input, effective, parsed, record);
|
|
946
948
|
if (clientId === "droid" && input.models.length > 0 && contribution.fragments.length === 0 && !record) {
|
|
947
949
|
return { failed: observationFailure("unsafe", "unsafe", "Factory Droid has no addressable models in the selected catalog") } as const;
|
|
948
950
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createClineIO } from "./cline-io";
|
|
12
12
|
import { parseClineDocument } from "./cline-document";
|
|
13
|
-
import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
13
|
+
import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, droidDefaultsFromOwnedRows, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
14
14
|
import type { ConfigFormat } from "../clients/config-export";
|
|
15
15
|
import type { OcxConfig } from "../types";
|
|
16
16
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
@@ -449,6 +449,7 @@ export interface IntegrationStateInput {
|
|
|
449
449
|
models: readonly ExportModel[];
|
|
450
450
|
config: OcxConfig;
|
|
451
451
|
port: number;
|
|
452
|
+
droidReasoningDefaults?: Record<string, string>;
|
|
452
453
|
env?: NodeJS.ProcessEnv;
|
|
453
454
|
home?: string;
|
|
454
455
|
/** The whole integration state store, bound to one root. */
|
|
@@ -462,7 +463,8 @@ export function exportContextOf(input: {
|
|
|
462
463
|
models: readonly ExportModel[];
|
|
463
464
|
config: OcxConfig;
|
|
464
465
|
port: number;
|
|
465
|
-
|
|
466
|
+
droidReasoningDefaults?: Record<string, string>;
|
|
467
|
+
}): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig; droidReasoningDefaults?: Record<string, string> } {
|
|
466
468
|
return {
|
|
467
469
|
/*
|
|
468
470
|
* Composed through the SAME helper `ocx export` uses. Interpolating the
|
|
@@ -476,9 +478,41 @@ export function exportContextOf(input: {
|
|
|
476
478
|
baseUrl: opencodeProxyBaseUrl(input.port, input.config.hostname, input.config),
|
|
477
479
|
models: input.models,
|
|
478
480
|
config: input.config,
|
|
481
|
+
...(input.droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: input.droidReasoningDefaults }),
|
|
479
482
|
};
|
|
480
483
|
}
|
|
481
484
|
|
|
485
|
+
export function buildIntegrationContribution(
|
|
486
|
+
input: IntegrationStateInput,
|
|
487
|
+
effective: IntegrationTarget,
|
|
488
|
+
parsed: unknown,
|
|
489
|
+
record: OwnershipRecord | null,
|
|
490
|
+
): ManagedContribution {
|
|
491
|
+
const context = exportContextOf(input);
|
|
492
|
+
if (input.clientId === "droid" && input.droidReasoningDefaults === undefined && record?.configPath === effective.configPath) {
|
|
493
|
+
const inherited = recordedDroidContributionMatches(parsed, record)
|
|
494
|
+
? droidDefaultsFromOwnedRows(context, parsed, record.fragmentPaths)
|
|
495
|
+
: {};
|
|
496
|
+
if (Object.keys(inherited).length > 0) context.droidReasoningDefaults = inherited;
|
|
497
|
+
}
|
|
498
|
+
return effective.buildContribution(context);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function recordedDroidContributionMatches(document: unknown, record: OwnershipRecord): boolean {
|
|
502
|
+
try {
|
|
503
|
+
const fragments = record.fragmentPaths.flatMap(path => {
|
|
504
|
+
const value = readPath(document, path);
|
|
505
|
+
return value === undefined ? [] : [{ path, value }];
|
|
506
|
+
});
|
|
507
|
+
if (fragments.length !== record.fragmentPaths.length) return false;
|
|
508
|
+
const observed: ManagedContribution = { clientId: "droid", fragments };
|
|
509
|
+
return record.semanticBlockFingerprint === fingerprint(semanticContribution(observed))
|
|
510
|
+
|| record.blockFingerprint === fingerprint(canonicalContribution(observed));
|
|
511
|
+
} catch {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
482
516
|
let retriedThisProcess = false;
|
|
483
517
|
|
|
484
518
|
/**
|
|
@@ -535,9 +569,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
535
569
|
try {
|
|
536
570
|
// One resolution for both, so a client whose paths come from mutable state
|
|
537
571
|
// cannot report one account's install beside another account's config path.
|
|
538
|
-
const
|
|
539
|
-
const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home, context);
|
|
540
|
-
if (input.clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(paths.detectDir, context);
|
|
572
|
+
const paths = resolveStatePaths(input);
|
|
541
573
|
installed = io.statKind(paths.detectDir) === "dir";
|
|
542
574
|
if (input.clientId === "cline") io = createClineIO(io, paths.configPath, store);
|
|
543
575
|
/*
|
|
@@ -608,7 +640,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
608
640
|
const parsed = input.clientId === "cline"
|
|
609
641
|
? parseClineDocument(loaded.before)
|
|
610
642
|
: parseConfig(loaded.before, effective.format, EXPORT_CLIENTS[input.clientId].jsonc ? { jsonc: true } : undefined);
|
|
611
|
-
const contribution =
|
|
643
|
+
const contribution = buildIntegrationContribution(input, effective, parsed, record);
|
|
612
644
|
if (input.clientId === "droid" && input.models.length > 0 && contribution.fragments.length === 0
|
|
613
645
|
&& (!record || record.configPath !== configPath)) {
|
|
614
646
|
return { clientId: input.clientId, state: "unsafe", installed, configPath,
|
|
@@ -650,3 +682,33 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
650
682
|
...retention,
|
|
651
683
|
};
|
|
652
684
|
}
|
|
685
|
+
|
|
686
|
+
function resolveStatePaths(input: IntegrationStateInput): { configPath: string; detectDir: string } {
|
|
687
|
+
const context = exportContextOf(input);
|
|
688
|
+
const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home, context);
|
|
689
|
+
if (input.clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(paths.detectDir, context);
|
|
690
|
+
return paths;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function readOwnedDroidReasoningDefaults(input: IntegrationStateInput): Record<string, string> {
|
|
694
|
+
if (input.clientId !== "droid") return {};
|
|
695
|
+
const store = input.store ?? createIntegrationStateStore();
|
|
696
|
+
const io = input.io ?? store.io();
|
|
697
|
+
try {
|
|
698
|
+
const paths = resolveStatePaths(input);
|
|
699
|
+
const record = store.readRecords().droid;
|
|
700
|
+
if (!record || record.clientId !== "droid" || record.configPath !== paths.configPath) return {};
|
|
701
|
+
const effective = resolveIntegrationTarget({
|
|
702
|
+
clientId: "droid", configPath: paths.configPath, io, record, env: input.env, home: input.home,
|
|
703
|
+
});
|
|
704
|
+
const loaded = loadTarget(io, effective.configPath);
|
|
705
|
+
if (!loaded.ok) return {};
|
|
706
|
+
const parsed = parseConfig(loaded.before, "json");
|
|
707
|
+
if (parsed === PARSE_FAILED) return {};
|
|
708
|
+
if (!recordedDroidContributionMatches(parsed, record)) return {};
|
|
709
|
+
return droidDefaultsFromOwnedRows(exportContextOf(input), parsed, record.fragmentPaths);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (error instanceof ClientPathError) return {};
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
@@ -18,7 +18,7 @@ import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
|
18
18
|
import { detachedConfigSnapshot } from "../config/admitted-identity";
|
|
19
19
|
import { copyPlainData } from "../lib/plain-data";
|
|
20
20
|
import type { OcxConfig } from "../types";
|
|
21
|
-
import { defaultIntegrationIO, loadTarget, type IntegrationIO } from "./config-io";
|
|
21
|
+
import { defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
22
22
|
import { inspectKiloCandidates } from "./kilo-candidates";
|
|
23
23
|
import {
|
|
24
24
|
fingerprint,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
type IntegrationClientId,
|
|
44
44
|
} from "./registry";
|
|
45
45
|
import { declaredIntegrationTarget } from "./target";
|
|
46
|
-
import { exportContextOf } from "./state";
|
|
46
|
+
import { buildIntegrationContribution, exportContextOf } from "./state";
|
|
47
47
|
import type { IntegrationState } from "./state";
|
|
48
48
|
import { serializeDocument, UnserializableValueError } from "./serialize";
|
|
49
49
|
import { ClientPathError } from "../clients/config-export";
|
|
@@ -713,7 +713,11 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
713
713
|
// exact bytes when the snapshot was taken. Re-deriving it from the file would
|
|
714
714
|
// mean guessing which entries are ours, and a wrong guess deletes a user's.
|
|
715
715
|
const restoredRecord = entry.priorRecord;
|
|
716
|
-
const fresh =
|
|
716
|
+
const fresh = buildIntegrationContribution(
|
|
717
|
+
{ ...input, droidReasoningDefaults: undefined }, rowTarget,
|
|
718
|
+
clientId === "droid" && restoredText !== null ? parseConfig(restoredText, "json") : undefined,
|
|
719
|
+
restoredRecord,
|
|
720
|
+
);
|
|
717
721
|
/*
|
|
718
722
|
* Does the restored record actually describe the restored bytes?
|
|
719
723
|
*
|
|
@@ -812,6 +816,12 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
812
816
|
if (!models.ok) {
|
|
813
817
|
throw new UncopyableIntegrationInputError("the model roster could not be captured for this write");
|
|
814
818
|
}
|
|
819
|
+
const droidReasoningDefaults = input.droidReasoningDefaults === undefined
|
|
820
|
+
? undefined
|
|
821
|
+
: copyPlainData(input.droidReasoningDefaults);
|
|
822
|
+
if (droidReasoningDefaults !== undefined && !droidReasoningDefaults.ok) {
|
|
823
|
+
throw new UncopyableIntegrationInputError("Droid reasoning defaults could not be captured for this write");
|
|
824
|
+
}
|
|
815
825
|
/*
|
|
816
826
|
* One resolution for both paths. Aside derives them from the account id in
|
|
817
827
|
* its manifest, so two independent calls could verify one account's install
|
|
@@ -828,7 +838,11 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
828
838
|
* checked in one state and written from another, which is the substitution the fingerprint
|
|
829
839
|
* exists to prevent. Copying both here gives the plan and the document one input.
|
|
830
840
|
*/
|
|
831
|
-
return {
|
|
841
|
+
return {
|
|
842
|
+
...input, config, models: models.value,
|
|
843
|
+
...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: droidReasoningDefaults.value }),
|
|
844
|
+
env, home, store, io, resolvedPaths,
|
|
845
|
+
};
|
|
832
846
|
}
|
|
833
847
|
|
|
834
848
|
function tryFreezeIntegrationInput(input: IntegrationWriteInput):
|
|
@@ -65,6 +65,23 @@ export function canonicalHttpOrigin(value: unknown): string | null {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Standalone pairing is only for this process's configured literal loopback origin.
|
|
70
|
+
* No CORS entry, public hub URL, wildcard bind, alias or client role can widen it.
|
|
71
|
+
* The CLI separately checks the attested runtime's actual port before requesting a grant.
|
|
72
|
+
*/
|
|
73
|
+
export function standaloneGuiPairingOrigin(config: {
|
|
74
|
+
runtimeRole?: string;
|
|
75
|
+
hostname?: string;
|
|
76
|
+
port: number;
|
|
77
|
+
}): string | null {
|
|
78
|
+
if ((config.runtimeRole ?? "standalone") !== "standalone") return null;
|
|
79
|
+
const hostname = config.hostname ?? "127.0.0.1";
|
|
80
|
+
if (hostname !== "127.0.0.1" && hostname !== "::1") return null;
|
|
81
|
+
if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) return null;
|
|
82
|
+
return new URL(`http://${hostname === "::1" ? "[::1]" : hostname}:${config.port}`).origin;
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
function capabilityPayload(
|
|
69
86
|
nonce: string,
|
|
70
87
|
method: string,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
2
|
+
const BLOCKED_FORWARDED_CLIENT_HEADERS = new Set([
|
|
3
|
+
"authorization",
|
|
4
|
+
"chatgpt-account-id",
|
|
5
|
+
"connection",
|
|
6
|
+
"content-length",
|
|
7
|
+
"content-type",
|
|
8
|
+
"cookie",
|
|
9
|
+
"expect",
|
|
10
|
+
"host",
|
|
11
|
+
"keep-alive",
|
|
12
|
+
"proxy-authorization",
|
|
13
|
+
"proxy-connection",
|
|
14
|
+
"set-cookie",
|
|
15
|
+
"te",
|
|
16
|
+
"trailer",
|
|
17
|
+
"transfer-encoding",
|
|
18
|
+
"upgrade",
|
|
19
|
+
"x-amz-security-token",
|
|
20
|
+
"x-api-key",
|
|
21
|
+
"api-key",
|
|
22
|
+
"x-goog-api-key",
|
|
23
|
+
"x-oai-attestation",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const ALLOWED_FORWARDED_CLIENT_HEADERS = new Set([
|
|
27
|
+
"originator", "x-client-request-id", "x-codex-app-version", "user-agent",
|
|
28
|
+
]);
|
|
29
|
+
const MAX_FORWARDED_CLIENT_HEADERS = 64;
|
|
30
|
+
|
|
31
|
+
/** Normalize one opt-in caller header while refusing credential and transport-owned fields. */
|
|
32
|
+
export function normalizeForwardedClientHeaderName(value: unknown): string | null {
|
|
33
|
+
if (typeof value !== "string") return null;
|
|
34
|
+
const name = value.trim().toLowerCase();
|
|
35
|
+
if (!name || !HEADER_NAME_PATTERN.test(name) || !ALLOWED_FORWARDED_CLIENT_HEADERS.has(name)) return null;
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Validate provider.forwardClientHeaders without ever admitting credential-bearing headers. */
|
|
40
|
+
export function providerForwardClientHeadersConfigError(value: unknown): string | null {
|
|
41
|
+
if (value === undefined) return null;
|
|
42
|
+
if (!Array.isArray(value)) return "forwardClientHeaders must be an array of header names";
|
|
43
|
+
if (value.length > MAX_FORWARDED_CLIENT_HEADERS) {
|
|
44
|
+
return `forwardClientHeaders must contain at most ${MAX_FORWARDED_CLIENT_HEADERS} names`;
|
|
45
|
+
}
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
for (const raw of value) {
|
|
48
|
+
if (typeof raw !== "string" || !raw.trim() || !HEADER_NAME_PATTERN.test(raw.trim())) {
|
|
49
|
+
return "forwardClientHeaders must use valid HTTP header names";
|
|
50
|
+
}
|
|
51
|
+
const name = raw.trim().toLowerCase();
|
|
52
|
+
if (BLOCKED_FORWARDED_CLIENT_HEADERS.has(name)) {
|
|
53
|
+
return `forwardClientHeaders must not include credential or transport-owned header "${raw.trim()}"`;
|
|
54
|
+
}
|
|
55
|
+
if (!ALLOWED_FORWARDED_CLIENT_HEADERS.has(name)) {
|
|
56
|
+
return "forwardClientHeaders must use the supported client metadata allowlist";
|
|
57
|
+
}
|
|
58
|
+
if (seen.has(name)) return `forwardClientHeaders must not repeat header "${raw.trim()}"`;
|
|
59
|
+
seen.add(name);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { detectClaudeCodeToken, hasClaudeCredentialContinuity } from "./local-token-detect";
|
|
2
|
+
import { normalizeAnthropicIdentity, resolveAnthropicAccountIdentity, type AnthropicIdentityResolver } from "./anthropic-identity";
|
|
3
|
+
import { credentialGeneration, type AuthStore } from "./store";
|
|
4
|
+
import { REFRESH_SKEW_MS } from "./refresh-policy";
|
|
5
|
+
import type { OAuthCredentials, ProviderAccountSet } from "./types";
|
|
6
|
+
|
|
7
|
+
export type ClaudeCredentialObservation =
|
|
8
|
+
| { kind: "absent" }
|
|
9
|
+
| { kind: "unresolved" | "different"; diskGeneration: string }
|
|
10
|
+
| { kind: "adopt"; credential: OAuthCredentials; diskGeneration: string };
|
|
11
|
+
|
|
12
|
+
export async function newerClaudeCredential(
|
|
13
|
+
stored: OAuthCredentials, now: number, signal?: AbortSignal,
|
|
14
|
+
resolveIdentity: AnthropicIdentityResolver = resolveAnthropicAccountIdentity,
|
|
15
|
+
): Promise<ClaudeCredentialObservation> {
|
|
16
|
+
if (stored.source !== "local-cli") return { kind: "absent" };
|
|
17
|
+
const disk = detectClaudeCodeToken();
|
|
18
|
+
if (!disk || !Number.isFinite(disk.expires) || disk.expires <= now + REFRESH_SKEW_MS
|
|
19
|
+
|| credentialGeneration(disk) === credentialGeneration(stored)) return { kind: "absent" };
|
|
20
|
+
const diskGeneration = credentialGeneration(disk);
|
|
21
|
+
const before = normalizeAnthropicIdentity(stored.anthropicIdentity, stored.access);
|
|
22
|
+
if (hasClaudeCredentialContinuity(stored, disk)) {
|
|
23
|
+
return { kind: "adopt", diskGeneration: credentialGeneration(disk), credential: {
|
|
24
|
+
...stored, ...disk, anthropicIdentity: disk.access === stored.access ? before : undefined,
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
27
|
+
// Independent observations share a deadline through the caller signal; neither extends the other.
|
|
28
|
+
const [oldProof, newProof] = await Promise.all([
|
|
29
|
+
before ?? resolveIdentity(stored.access, signal), resolveIdentity(disk.access, signal),
|
|
30
|
+
]);
|
|
31
|
+
const oldIdentity = normalizeAnthropicIdentity(oldProof, stored.access);
|
|
32
|
+
const newIdentity = normalizeAnthropicIdentity(newProof, disk.access);
|
|
33
|
+
if (!oldIdentity || !newIdentity) return { kind: "unresolved", diskGeneration };
|
|
34
|
+
if (oldIdentity.accountUuid !== newIdentity.accountUuid) return { kind: "different", diskGeneration };
|
|
35
|
+
return { kind: "adopt", diskGeneration: credentialGeneration(disk), credential: {
|
|
36
|
+
...stored, ...disk, accountId: newIdentity.accountUuid, anthropicIdentity: newIdentity,
|
|
37
|
+
} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Capture primitives before network awaits; check them again inside the serialized writer. */
|
|
41
|
+
export function captureAnthropicCredentialOwner(set: ProviderAccountSet, accountId: string) {
|
|
42
|
+
const row = set.accounts.find(account => account.id === accountId)!;
|
|
43
|
+
const loginId = row.loginId;
|
|
44
|
+
const generation = credentialGeneration(row.credential);
|
|
45
|
+
const metadata = identityMetadata(row.credential);
|
|
46
|
+
const needsReauth = row.needsReauth;
|
|
47
|
+
const selection = { id: set.activeAccountId, revision: set.selectionRevision };
|
|
48
|
+
return (store: AuthStore, diskGeneration?: string, allowPaused = false): boolean => {
|
|
49
|
+
const current = store["anthropic"];
|
|
50
|
+
const account = current?.accounts.find(candidate => candidate.id === accountId);
|
|
51
|
+
if (!account || (!allowPaused && account.paused) || account.loginId !== loginId
|
|
52
|
+
|| account.needsReauth !== needsReauth || credentialGeneration(account.credential) !== generation
|
|
53
|
+
|| identityMetadata(account.credential) !== metadata) return false;
|
|
54
|
+
if (diskGeneration !== undefined) {
|
|
55
|
+
if (current?.activeAccountId !== selection.id || current?.selectionRevision !== selection.revision) return false;
|
|
56
|
+
const disk = detectClaudeCodeToken();
|
|
57
|
+
if (!disk || disk.expires <= Date.now() + REFRESH_SKEW_MS || credentialGeneration(disk) !== diskGeneration) return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function identityMetadata(credential: OAuthCredentials): string {
|
|
64
|
+
return JSON.stringify([credential.source, credential.accountId, credential.email, credential.anthropicIdentity]);
|
|
65
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Private account proof from an authenticated Anthropic response, bound to its exact bearer. */
|
|
2
|
+
import { hasClaudeCredentialContinuity } from "./local-token-detect";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
5
|
+
import { CLAUDE_CLI_USER_AGENT } from "../providers/claude-cli-identity";
|
|
6
|
+
import type { OAuthCredentials } from "./types";
|
|
7
|
+
|
|
8
|
+
const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
9
|
+
const PROFILE_TIMEOUT_MS = 10_000;
|
|
10
|
+
const PROFILE_MAX_BYTES = 65_536;
|
|
11
|
+
export type AnthropicIdentity = NonNullable<OAuthCredentials["anthropicIdentity"]>;
|
|
12
|
+
export type AnthropicIdentityResolver = (access: string, signal?: AbortSignal) => Promise<AnthropicIdentity | undefined>;
|
|
13
|
+
|
|
14
|
+
export function bindAnthropicIdentity(access: string, accountUuid: unknown): AnthropicIdentity | undefined {
|
|
15
|
+
if (!access || typeof accountUuid !== "string" || !accountUuid.length || accountUuid.length > 128
|
|
16
|
+
|| accountUuid !== accountUuid.trim() || /[\x00-\x1f\x7f]/.test(accountUuid)) return undefined;
|
|
17
|
+
return { v: 1, accountUuid, bearerSha256: createHash("sha256").update(access).digest("hex") };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeAnthropicIdentity(value: unknown, access: string): AnthropicIdentity | undefined {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
22
|
+
const proof = value as Partial<AnthropicIdentity>;
|
|
23
|
+
const bound = bindAnthropicIdentity(access, proof.accountUuid);
|
|
24
|
+
return proof.v === 1 && bound && proof.bearerSha256 === bound.bearerSha256 ? bound : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Fixed destination, no redirects, bounded body and deadline; failures never include response data. */
|
|
28
|
+
export const resolveAnthropicAccountIdentity: AnthropicIdentityResolver = async (access, signal) => {
|
|
29
|
+
if (!access || signal?.aborted) return undefined;
|
|
30
|
+
const deadline = AbortSignal.timeout(PROFILE_TIMEOUT_MS);
|
|
31
|
+
const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(PROFILE_URL, {
|
|
34
|
+
method: "GET", redirect: "error", signal: combined,
|
|
35
|
+
headers: { Authorization: `Bearer ${access}`, Accept: "application/json",
|
|
36
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
37
|
+
"Cache-Control": "no-cache", "User-Agent": CLAUDE_CLI_USER_AGENT },
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok || response.redirected) {
|
|
40
|
+
void response.body?.cancel().catch(() => {});
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const body = await readBoundedResponseBody(response, {
|
|
44
|
+
signal: combined, maxBytes: PROFILE_MAX_BYTES, fatalUtf8: true,
|
|
45
|
+
totalTimeoutMs: PROFILE_TIMEOUT_MS, inactivityTimeoutMs: PROFILE_TIMEOUT_MS,
|
|
46
|
+
});
|
|
47
|
+
if (body.truncated || body.oversized || body.timedOut || combined.aborted) return undefined;
|
|
48
|
+
const data: unknown = JSON.parse(body.text);
|
|
49
|
+
if (!data || typeof data !== "object" || !("account" in data)) return undefined;
|
|
50
|
+
const account = data.account;
|
|
51
|
+
return account && typeof account === "object" && "uuid" in account
|
|
52
|
+
? bindAnthropicIdentity(access, account.uuid) : undefined;
|
|
53
|
+
} catch {
|
|
54
|
+
// Unavailable identity is an explicit unresolved result, never an ownership guess.
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A changed access bearer cannot inherit the previous bearer's proof. */
|
|
60
|
+
export function mergeAnthropicIdentity(fresh: OAuthCredentials, previous: OAuthCredentials): Pick<OAuthCredentials, "anthropicIdentity"> {
|
|
61
|
+
const before = normalizeAnthropicIdentity(previous.anthropicIdentity, previous.access);
|
|
62
|
+
const after = normalizeAnthropicIdentity(fresh.anthropicIdentity, fresh.access);
|
|
63
|
+
if (before && after && before.accountUuid !== after.accountUuid) {
|
|
64
|
+
throw new Error("Anthropic credential identity changed; explicit account login required");
|
|
65
|
+
}
|
|
66
|
+
return { anthropicIdentity: after ?? (fresh.access === previous.access ? before : undefined) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Explicit imports enrich only a proven slot; labels and paths never establish ownership. */
|
|
70
|
+
export function matchesAnthropicImport(stored: OAuthCredentials, imported: OAuthCredentials): boolean {
|
|
71
|
+
const before = normalizeAnthropicIdentity(stored.anthropicIdentity, stored.access);
|
|
72
|
+
const after = normalizeAnthropicIdentity(imported.anthropicIdentity, imported.access);
|
|
73
|
+
if (before && after) return before.accountUuid === after.accountUuid;
|
|
74
|
+
return hasClaudeCredentialContinuity(stored, imported);
|
|
75
|
+
}
|
|
@@ -18,6 +18,7 @@ import { anthropicFamilyRejected, anthropicFamilyRetryAt, anthropicModelExhauste
|
|
|
18
18
|
* Token refresh failures retain the existing store needsReauth policy.
|
|
19
19
|
*/
|
|
20
20
|
import { anthropicRatePauseUntil, clearAnthropicRatePauses, classifyAnthropic429, anthropicRetryAfterMs, pauseAnthropicRateAdmission } from "./anthropic-rate-limit-policy";
|
|
21
|
+
import { REFRESH_SKEW_MS } from "./refresh-policy";
|
|
21
22
|
import { createHash } from "node:crypto";
|
|
22
23
|
import { captureOAuthAccountSelection, commitOAuthAccountSelection, credentialGeneration, getAccountSet, getAccountCredential, getAccountCredentialWithStatus } from "./store";
|
|
23
24
|
import type { OAuthAccessSnapshot } from "./index";
|
|
@@ -353,7 +354,6 @@ function usageScore(config: OcxConfig, accountId: string, model?: string): numbe
|
|
|
353
354
|
}
|
|
354
355
|
}
|
|
355
356
|
|
|
356
|
-
const TOKEN_SKEW_MS = 60_000;
|
|
357
357
|
|
|
358
358
|
/** Background `local-cli` slots with expired access are not pool-eligible (identity adoption risk). */
|
|
359
359
|
function isPoolCredentialUsable(accountId: string, now: number): boolean {
|
|
@@ -361,7 +361,7 @@ function isPoolCredentialUsable(accountId: string, now: number): boolean {
|
|
|
361
361
|
if (!cred) return false;
|
|
362
362
|
if (cred.source !== "local-cli") return true;
|
|
363
363
|
if (canRefreshAnthropicPoolAccount(accountId)) return true;
|
|
364
|
-
return cred.expires > now +
|
|
364
|
+
return cred.expires > now + REFRESH_SKEW_MS;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
export function getEligibleAnthropicAccounts(now = Date.now(), model?: string): string[] {
|
|
@@ -1093,7 +1093,7 @@ export async function getAnthropicPoolAccessToken(accountId: string): Promise<st
|
|
|
1093
1093
|
const { OAuthLoginRequiredError } = await import("./index");
|
|
1094
1094
|
throw new OAuthLoginRequiredError(PROVIDER);
|
|
1095
1095
|
}
|
|
1096
|
-
if (stored.expires > Date.now() +
|
|
1096
|
+
if (stored.expires > Date.now() + REFRESH_SKEW_MS) return stored.access;
|
|
1097
1097
|
if (!canRefreshAnthropicPoolAccount(accountId)) {
|
|
1098
1098
|
throw new Error("background local-cli token expired; refuse CLI-adopting refresh for pool");
|
|
1099
1099
|
}
|
package/src/oauth/anthropic.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Anthropic OAuth flow (Claude Pro/Max). Ported from jawcode oauth/anthropic.ts. */
|
|
2
2
|
import { OAuthCallbackFlow } from "./callback-server";
|
|
3
|
+
import { bindAnthropicIdentity, resolveAnthropicAccountIdentity } from "./anthropic-identity";
|
|
3
4
|
import { generatePKCE } from "./pkce";
|
|
4
5
|
import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
|
|
5
6
|
|
|
@@ -96,6 +97,7 @@ function credsFrom(data: AnthropicTokenResponse, refreshFallback?: string): OAut
|
|
|
96
97
|
refresh: data.refresh_token || refreshFallback || "",
|
|
97
98
|
access: data.access_token,
|
|
98
99
|
expires,
|
|
100
|
+
anthropicIdentity: bindAnthropicIdentity(data.access_token, accountUuid),
|
|
99
101
|
accountId: typeof accountUuid === "string" && accountUuid.length > 0 ? accountUuid : undefined,
|
|
100
102
|
email: typeof email === "string" && email.length > 0 ? email : undefined,
|
|
101
103
|
};
|
|
@@ -159,7 +161,11 @@ export async function loginAnthropic(
|
|
|
159
161
|
const local = detectClaudeCodeToken();
|
|
160
162
|
if (local) {
|
|
161
163
|
ctrl.onProgress?.("Found Claude Code token, importing automatically");
|
|
162
|
-
if (local.expires >= Date.now() + 60_000)
|
|
164
|
+
if (local.expires >= Date.now() + 60_000) {
|
|
165
|
+
const identity = await resolveAnthropicAccountIdentity(local.access, ctrl.signal);
|
|
166
|
+
ctrl.signal?.throwIfAborted();
|
|
167
|
+
return { ...local, ...(identity ? { accountId: identity.accountUuid, anthropicIdentity: identity } : {}) };
|
|
168
|
+
}
|
|
163
169
|
try {
|
|
164
170
|
return { ...(await refreshAnthropicToken(local.refresh)), source: "local-cli" };
|
|
165
171
|
} catch (error) {
|
|
@@ -734,3 +734,24 @@ export function clearGenericFailoverHealth(providerName?: string): void {
|
|
|
734
734
|
if (key.startsWith(`${providerName}\u0000`)) health.delete(key);
|
|
735
735
|
}
|
|
736
736
|
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Retire one account's superseded auth evidence after an explicit (re-)login. An
|
|
740
|
+
* auth cooldown recorded against the old grant must not hold out the fresh
|
|
741
|
+
* credential, so it leaves with the login rather than lingering until its
|
|
742
|
+
* deadline. Rate, quota, and suspension evidence is preserved: a re-login says
|
|
743
|
+
* nothing about those verdicts, and dropping them would make a throttled account
|
|
744
|
+
* eligible early. Entries bound to the still-current generation stay as well.
|
|
745
|
+
*/
|
|
746
|
+
export function clearGenericFailoverHealthForAccount(providerName: string, accountId: string): void {
|
|
747
|
+
const live = getAccountSet(providerName)?.accounts.find(row => row.id === accountId);
|
|
748
|
+
const current = live ? credentialGeneration(live.credential) : undefined;
|
|
749
|
+
const bare = `${providerName}\u0000${accountId}`;
|
|
750
|
+
for (const key of [...health.keys()]) {
|
|
751
|
+
if (key !== bare && !key.startsWith(`${bare}\u0000`)) continue;
|
|
752
|
+
const entry = health.get(key);
|
|
753
|
+
if (!entry || entry.cooldownSource !== "auth") continue;
|
|
754
|
+
if (current !== undefined && entry.identity === current) continue;
|
|
755
|
+
health.delete(key);
|
|
756
|
+
}
|
|
757
|
+
}
|